Jenis test dan test double untuk Product Catalog
Unit Test vs Integration Test vs E2E, mock vs stub vs fake, WebApplicationFactory, dan apa yang sebaiknya di-mock
ProductService dan endpoint POST /api/products.
1. Tiga jenis test, satu piramida
Microsoft: "Unit tests are used to test isolated software components, such as individual class methods. Integration tests confirm that two or more app components work together to produce an expected result, possibly including every component required to fully process a request." [1] Integration test biasanya menyentuh "Database · File system · Network appliances · Request-response pipeline". [1]
| Jenis | Yang diuji di Product Catalog | Kecepatan | Padanan Node |
|---|---|---|---|
| Unit Test | ProductService.CreateAsync, repository di-mock dengan Moq | Milidetik [2] | Jest + jest.fn() [7] |
| Integration Test | POST /api/products lewat pipeline HTTP, Controller, Service, sampai database test | Lebih lambat: "Take longer to run" [1] | Supertest + database test [8] |
| E2E | Browser membuka form Angular, submit, data muncul di daftar | Paling lambat, "notoriously flaky" [4] | Playwright / Cypress |
Testing pyramid menurut Ham Vocke (artikel di martinfowler.com) punya dua aturan: "Write tests with different granularity" dan "The more high-level you get the fewer tests you should have." [4] Microsoft bahkan memberi aturan praktis: "If a behavior can be tested using either a unit test or an integration test, choose the unit test." [1] Jadi aturan harga ≤ 0 cukup di unit test (lesson 0009); integration test cukup membuktikan bahwa request yang salah benar-benar berakhir sebagai 400.
Di GLOSSARY: Integration Test = API sungguhan lewat WebApplicationFactory; bukan E2E (yang dari browser sampai DB).
Pewawancara sering sengaja mencampur dua istilah ini.
2. Mock, stub, fake: semuanya test double
"Test Double is a generic term for any case where you replace a production object for testing purposes." [3] Bedanya ada di peran objek itu dalam test:
| Istilah | Definisi (Fowler [3]) | Contoh Product Catalog | Padanan Jest |
|---|---|---|---|
| Stub | "provide canned answers to calls made during the test" | _repo.Setup(r => r.ExistsByNameAsync("Keyboard")).ReturnsAsync(true) | jest.fn().mockReturnValue(true) |
| Mock | "pre-programmed with expectations" dan "checked during verification to ensure they got all the calls they were expecting" | _repo.Verify(r => r.AddAsync(It.IsAny<Product>()), Times.Never) | expect(fn).toHaveBeenCalled() |
| Fake | "actually have working implementations, but usually take some shortcut" (contohnya "an InMemoryTestDatabase") | SQLite in-memory menggantikan SQL Server | Repository berbasis array, SQLite :memory: |
| Dummy | "passed around but never actually used" | null / Mock.Of<ILogger<ProductService>>() sekadar mengisi constructor | {} sebagai argumen |
Kalimat kunci untuk interview: "only mocks insist upon behavior verification. The other doubles can, and usually do, use state verification."
[5]
Microsoft merangkumnya: "You run Assert operations against a mock object, but not against a stub."
[2]
Jebakan istilah: objek new Mock<IProductRepository>() dari Moq [6]
belum tentu mock. Kalau Anda hanya memakai Setup/Returns dan tidak pernah Verify, perannya stub.
Microsoft juga mencatat bahwa dokumentasi .NET memakai "fake" sebagai istilah umum ("a fake can be a stub or a mock"), sementara Fowler/Meszaros memakainya lebih sempit.
[2]
Di interview, sebut definisi Fowler lalu tambahkan catatan ini: itu tanda Anda paham, bukan hafal.
3. WebApplicationFactory: menguji API tanpa deploy
"ASP.NET Core supports integration tests using a unit test framework with a test web host and an in-memory test server."
WebApplicationFactory<TEntryPoint> "is used to create a TestServer for the integration tests. TEntryPoint is the entry point class of the SUT, usually Program.cs."
[1]
Ia ada di paket Microsoft.AspNetCore.Mvc.Testing (sudah ada di setup ROADMAP).
// Program.cs (paling bawah): class Program implisit dibuat public supaya bisa jadi TEntryPoint
public partial class Program { }
// Test project
public class ApiFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder) =>
builder.ConfigureServices(services =>
{
// dijalankan SETELAH Program.cs: hapus DbContext SQL Server, daftarkan SQLite in-memory (fake)
});
}
public class ProductsApiTests(ApiFactory factory) : IClassFixture<ApiFactory>
{
[Fact]
public async Task Post_InvalidPrice_Returns400()
{
var client = factory.CreateClient(); // Arrange: HttpClient ke TestServer
var response = await client.PostAsJsonAsync("/api/products",
new CreateProductRequest("Mouse", 0, 1, 1)); // Act: request HTTP sungguhan, in-memory
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); // Assert: status code
}
}
- Kenapa
public partial class Program? Microsoft: "Expose the implicitly definedProgramclass to the test project", lewatInternalsVisibleToatau partial class public. [1] - Kenapa penggantian DbContext berhasil? "The test app's
builder.ConfigureServicescallback is executed after the app'sProgram.cscode is executed." [1] - Ganti satu service untuk satu test saja:
factory.WithWebHostBuilder(b => b.ConfigureTestServices(...)). [1] IClassFixtureberbagi satu factory untuk semua test di class itu ("provide shared object instances across the tests in the class"). [1]
Padanan Node: Supertest, request(app).post("/api/products"). Supertest juga tidak butuh server yang sudah listen:
"if the server is not already listening for connections then it is bound to an ephemeral port for you". [8]
Bedanya, TestServer bahkan tidak membuka port; semuanya in-memory.
4. Apa yang di-mock, apa yang tidak
| Mock / stub | Jangan di-mock |
|---|---|
Batas I/O di unit test: IProductRepository, HTTP client ke layanan lain, jam sistem (IDateTimeProvider).
Unit test yang baik itu "Isolated": "no dependencies on any outside factors, such as a file system or database."
[2]
|
Entity dan DTO (objek data biasa, pakai yang asli); SUT itu sendiri; method private ("write your tests against the public method")
[2];
dan DbSet untuk query, karena LINQ adalah "static extension method calls over IQueryable".
[9]
|
Untuk database di integration test, dokumentasi EF Core punya urutan saran yang jelas: utamakan database produksi asli (mis. lewat Testcontainers); kalau butuh test double, repository pattern; kalau tidak bisa, SQLite in-memory. Provider EF Core in-memory "is highly discouraged". [9] ROADMAP memakai SQLite karena cepat disiapkan; trade-off-nya: "testing against SQLite does not guarantee the same results as against SQL Server" (mis. SQLite case-sensitive, SQL Server tidak). [9] Menyebut trade-off ini di interview langsung terdengar mid-level.
Ini juga menyambung ke lesson 0011: salah satu argumen pro Repository di atas EF Core adalah kemudahan di-mock, dan dokumentasi EF Core sendiri menyebutnya "the only approach allowing the comprehensive and reliable stubbing/mocking of the data layer", dengan "significant cost". [9]
Latihan 1: Jenis test dan WebApplicationFactory
Jawab tanpa scroll ke atas. Soal-soal ini muncul lagi, diacak, di review campuran 0012.
Kuis di lesson ini adalah feedback cepat, bukan bukti penguasaan; yang dicatat hanya hasil di review campuran 0012.
Latihan 2: Mock, stub, fake, dan apa yang di-mock
Latihan 3: Ucapkan dengan suara keras
Pasang timer 60 detik untuk tiap pertanyaan dan jawab dengan suara keras, seolah sedang interview. Jangan buka jawaban contoh sebelum selesai.
"Apa beda Unit Test, Integration Test, dan E2E? Bagaimana Anda menguji API Product Catalog tanpa deploy?"
Bandingkan dengan jawaban contoh dan checklist-nya
"Unit test menguji satu komponen terisolasi, misalnya ProductService dengan IProductRepository di-mock pakai Moq; cepat, dalam milidetik, tanpa database.
Integration test memastikan beberapa komponen bekerja sama: pipeline HTTP, Controller, Service, sampai database.
E2E menguji dari browser Angular sampai database, paling lambat dan paling rapuh.
Itulah testing pyramid: banyak unit test, lebih sedikit integration test, sedikit sekali E2E. Microsoft juga bilang kalau satu perilaku bisa diuji dua cara, pilih unit test.
Untuk API saya pakai WebApplicationFactory<Program>: ia menjalankan aplikasi di TestServer in-memory, CreateClient() memberi HttpClient, jadi tanpa deploy.
Di ConfigureWebHost saya ganti DbContext ke SQLite in-memory; idealnya SQL Server asli lewat Testcontainers karena dialeknya beda.
Di Node padanannya Supertest dengan request(app)."
Checklist: apakah Anda menyebut…
- Unit test: satu komponen terisolasi, dependensi I/O diganti test double, cepat?
- Integration test: dua komponen atau lebih bekerja sama, termasuk database/pipeline HTTP?
- E2E: dari UI sampai database, paling lambat dan rapuh?
- testing pyramid: makin tinggi levelnya makin sedikit jumlahnya; kalau bisa dua-duanya, pilih unit test?
WebApplicationFactory: TestServer in-memory +CreateClient(), tanpa deploy?- mengganti DbContext di
ConfigureWebHost(SQLite/Testcontainers) dan menyebut trade-off-nya?
Lulus kalau minimal 4 dari 6. Kalau kurang, ulangi besok pagi sebelum review 0012 (spacing).
"Apa beda mock, stub, dan fake? Apa yang Anda mock dan apa yang tidak?"
Bandingkan dengan jawaban contoh dan checklist-nya
"Ketiganya test double, pengganti objek produksi saat test.
Stub memberi jawaban siap, misalnya Setup(r => r.ExistsByNameAsync("Keyboard")).ReturnsAsync(true), dan saya tidak meng-assert apa pun terhadapnya.
Mock diverifikasi: test gagal kalau panggilan yang diharapkan tidak terjadi, misalnya Verify(r => r.AddAsync(It.IsAny<Product>()), Times.Never).
Jadi bedanya di Assert, bukan di library; objek Mock<T> Moq bisa berperan sebagai stub.
Fake punya implementasi kerja dengan jalan pintas, contohnya SQLite in-memory menggantikan SQL Server.
Yang saya mock adalah batas I/O: repository, HTTP client ke layanan lain, jam sistem.
Yang tidak: Entity dan DTO, SUT-nya sendiri, method private, dan DbSet untuk query karena LINQ adalah extension method statis.
Di Jest padanannya jest.fn().mockReturnValue(...) untuk stub dan expect(fn).toHaveBeenCalled() untuk mock."
Checklist: apakah Anda menyebut…
- test double sebagai istilah umum pengganti objek produksi?
- Stub: jawaban siap (
Setup/Returns), tidak di-assert? - Mock: diverifikasi panggilannya (
Verify/Times), bedanya di Assert bukan di library? - Fake: implementasi kerja dengan jalan pintas (SQLite in-memory)?
- yang di-mock: batas I/O seperti repository, HTTP client, jam sistem?
- yang tidak di-mock: Entity/DTO, SUT, method private,
DbSetuntuk query?
Lulus kalau minimal 4 dari 6.
Ringkasan untuk dicetak: Reference: Jenis test dan test double · Siklus TDD: Reference: Red-Green-Refactor · Istilah: GLOSSARY.md
Ada yang belum jelas? Tanyakan ke saya (agent) di terminal, misalnya: "kapan saya pakai ConfigureTestServices dibanding ConfigureServices?" atau "bagaimana Testcontainers dipasang di ApiFactory?"
Sumber: [1] Microsoft Learn: Integration tests in ASP.NET Core · [2] Microsoft Learn: Unit testing best practices for .NET · [3] Martin Fowler: TestDouble · [4] Ham Vocke (martinfowler.com): The Practical Test Pyramid · [5] Martin Fowler: Mocks Aren't Stubs · [6] Moq Quickstart · [7] Jest: Mock Functions · [8] SuperTest · [9] Microsoft Learn: EF Core, Choosing a testing strategy