Lesson 0009 · Hari 2 · Testing · ±13 menit

Red-Green-Refactor pada satu aturan Product

Siklusnya, kenapa harus merah dulu, Arrange-Act-Assert, dan nama test yang bisa dibaca

Target lesson ini: Anda bisa menjawab pertanyaan interview "Jelaskan red-green-refactor. Kenapa test harus terlihat gagal dulu?" dalam 60 detik, dengan contoh aturan "harga Product harus lebih dari 0". Sebagai bonus, Anda bisa mendemokan satu siklus lengkap di depan pewawancara tanpa catatan.

1. Tiga langkah, tiga warna

Martin Fowler merangkum TDD menjadi tiga langkah yang "often summarized as Red - Green - Refactor". [4] Kent Beck, yang mempopulerkan TDD, menambahkan satu langkah di depan: tulis dulu test list, daftar skenario yang ingin Anda cakup, lalu ambil satu per satu. [3]

LangkahYang Anda lakukanKesalahan yang disebut Beck [3]Padanan Node
Red"Turn exactly one item on the list into an actual, concrete, runnable test", jalankan, lihat gagalMengubah semua item list menjadi test sekaligus; test tanpa assertionSatu test() baru, jest --watch merah
Green"Change the code to make the test (& all previous tests) pass"Menghapus assertion; menyalin hasil hitungan ke nilai expected; "mixing refactoring into making the test pass"Kode minimal di services/
Refactor"Optionally refactor to improve the implementation design", test tetap hijau"abstracting too soon. Duplication is a hint, not a command."Ekstrak fungsi, ganti nama

Jebakan yang paling sering: "The most common way that I hear to screw up TDD is neglecting the third step." [4] Green boleh "jelek" justru karena ada Refactor sesudahnya. Kalau Refactor dilewati, yang tersisa hanya kode jelek yang kebetulan ber-test.

2. Satu siklus: "harga harus lebih dari 0"

Test list untuk ProductService.CreateAsync (dari ROADMAP Hari 2): Product valid tersimpan, harga ≤ 0 ditolak, Stock negatif ditolak, nama duplikat ditolak. Kita ambil satu: harga ≤ 0 ditolak.

🔴 Red: tulis test, jalankan, lihat gagal

public class ProductServiceTests
{
    private readonly Mock<IProductRepository> _repo = new();
    private readonly ProductService _sut;

    public ProductServiceTests() => _sut = new ProductService(_repo.Object);

    [Theory]
    [InlineData(0)]
    [InlineData(-10)]
    public async Task CreateAsync_PriceNotPositive_ThrowsValidationException(decimal price)
    {
        // Arrange
        var request = new CreateProductRequest("Keyboard", price, 5, 1);

        // Act
        var act = () => _sut.CreateAsync(request);

        // Assert
        await Assert.ThrowsAsync<ValidationException>(act);
        _repo.Verify(r => r.AddAsync(It.IsAny<Product>()), Times.Never);
    }
}

dotnet test → merah: diharapkan ValidationException, tapi tidak ada exception yang dilempar. Itu gagal karena alasan yang benar: perilakunya memang belum ada. Tutorial xUnit Microsoft memakai pola yang sama: "With TDD, you write a test that's known to fail. Then you update the target code to make the test pass." [2]

🟢 Green: kode paling sederhana

public async Task<ProductResponse> CreateAsync(CreateProductRequest request)
{
    if (request.Price <= 0) throw new ValidationException("Price must be greater than 0.");
    // ... kode create dari siklus sebelumnya
}

"Using the TDD approach, write only enough code so this test passes." [2] dotnet test → test ini dan semua test lama hijau.

🔵 Refactor: rapikan, test tetap hijau

// Setelah siklus Stock dan nama duplikat menambah dua if lagi:
private async Task ValidateAsync(CreateProductRequest r)
{
    if (r.Price <= 0) throw new ValidationException("Price must be greater than 0.");
    if (r.Stock < 0) throw new ValidationException("Stock cannot be negative.");
    if (await _repo.ExistsByNameAsync(r.Name)) throw new ValidationException("Product name already exists.");
}

Refactor tidak menambah perilaku, jadi tidak ada test baru; jalankan test lagi dan pastikan semuanya tetap hijau. Lalu kembali ke test list dan ambil item berikutnya. [3]

3. Kenapa harus merah dulu?

Test yang belum pernah Anda lihat gagal belum terbukti bisa gagal. Beberapa cara sebuah test selalu hijau tanpa menguji apa pun: assertion terlupa, method yang salah dipanggil, atau mock yang membuat jalur error tidak pernah tersentuh. Beck mencatat dua versinya sebagai kesalahan: "write tests without assertions just to get code coverage" dan "delete assertions so the test pretends to pass." [3]

Padanan Node: menulis expect(...) di Jest lalu langsung melihat centang hijau tanpa pernah merah adalah sinyal yang sama. Curigai test-nya, bukan rayakan kodenya.

4. Arrange-Act-Assert dan nama test

Microsoft: "Arrange your objects, create, and configure them as necessary · Act on an object · Assert that something is as expected." Keuntungannya: "you can clearly separate what is being tested from the Arrange and Assert tasks." [1] Act cukup satu per test; kalau ada dua input, pakai parameterized test. [1] Jangan pula menaruh if/for di dalam test: "Avoid coding logic in unit tests." [1]

KonvensixUnit (.NET)Padanan Jest
Nama test: method, skenario, hasil [1]CreateAsync_PriceNotPositive_ThrowsValidationExceptiondescribe("createProduct") + it("throws when price ≤ 0")
Satu kasus[Fact]test() / it()
Kode sama, input berbeda [2][Theory] + [InlineData(0)]test.each [5]
Menjalankan terus-menerusdotnet watch testjest --watch

Kenapa nama itu penting: test adalah dokumentasi, dan "when tests fail, you can see exactly which scenarios don't meet your expectations." [1] Dari daftar nama test saja, pewawancara bisa membaca aturan bisnis Product Anda.

Latihan 1: Urutkan satu siklus

Langkah-langkah di bawah sudah diacak. Pindahkan dengan ↑/↓ sampai urutannya benar untuk aturan harga, lalu tekan Periksa. Jangan scroll ke atas dulu.

  1. Ambil satu item dari test list: harga ≤ 0 ditolak
  2. Tulis CreateAsync_PriceNotPositive_ThrowsValidationException dengan Arrange-Act-Assert
  3. Jalankan dotnet test: merah, karena tidak ada exception yang dilempar
  4. Tambahkan satu if (request.Price <= 0) throw di CreateAsync
  5. Jalankan dotnet test: test ini dan semua test lama hijau
  6. Ekstrak validasi ke ValidateAsync, jalankan test lagi: tetap hijau
  7. Kembali ke test list, ambil perilaku berikutnya

Test list → Red (tulis, lalu lihat merah) → Green (kode minimal, semua test hijau) → Refactor (tetap hijau) → ulangi. Kesalahan urutan yang paling umum: menulis kode sebelum melihat merah, atau merapikan kode sebelum hijau (Beck: "mixing refactoring into making the test pass").

Latihan 2: Siklus dan "kenapa merah dulu"

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 3: Arrange-Act-Assert dan nama test

Latihan 4: Ucapkan dengan suara keras

Pasang timer 60 detik dan jawab dengan suara keras, seolah sedang interview. Jangan buka jawaban contoh sebelum selesai.

"Jelaskan red-green-refactor. Kenapa test harus terlihat gagal dulu?"
Bandingkan dengan jawaban contoh dan checklist-nya

"Red-green-refactor adalah siklus TDD. Red: saya tulis satu test kecil untuk satu perilaku yang belum ada, misalnya CreateAsync_PriceNotPositive_ThrowsValidationException, lalu jalankan dan lihat test itu gagal. Green: saya tulis kode paling sederhana supaya test itu dan semua test lama lulus, misalnya satu if (request.Price <= 0) throw. Refactor: saya rapikan kode, misalnya mengekstrak ValidateAsync, dengan test tetap hijau; tahap ini yang paling sering dilewati orang. Lalu ulangi untuk perilaku berikutnya. Test harus merah dulu karena itu bukti test-nya bisa mendeteksi perilaku yang hilang; test yang tidak pernah gagal bisa saja tidak menguji apa-apa, misalnya assertion-nya terlupa. Gagalnya juga harus karena alasan yang benar, bukan karena setup rusak. Di Node polanya sama dengan jest --watch: lihat merah, baru tulis kodenya."

Checklist: apakah Anda menyebut…

  • Red: satu test kecil untuk perilaku yang belum ada, dijalankan dan gagal?
  • Green: kode paling sederhana sampai test ini dan test lama lulus?
  • Refactor: rapikan kode dengan semua test tetap hijau (dan jangan dilewati)?
  • ulangi satu perilaku per siklus, dengan contoh konkret dari Product?
  • gagal dulu membuktikan test bisa mendeteksi perilaku yang hilang (bukan selalu hijau)?
  • gagal karena alasan yang benar, bukan setup rusak atau error compile?

Lulus kalau minimal 4 dari 6. Kalau kurang, ulangi besok pagi sebelum review 0012 (spacing).

Bonus (90 detik): demo tanpa catatan

"Demokan satu siklus TDD untuk aturan 'harga Product harus lebih dari 0': nama test, isi Arrange-Act-Assert, kode green, dan apa yang di-refactor."
Bandingkan dengan jawaban contoh dan checklist-nya

"Nama test: CreateAsync_PriceNotPositive_ThrowsValidationException, sebuah [Theory] dengan [InlineData(0)] dan [InlineData(-10)]. Arrange: mock IProductRepository dengan Moq dan request dengan harga dari parameter. Act: panggil CreateAsync(request) sekali. Assert: Assert.ThrowsAsync<ValidationException>, lalu Verify bahwa AddAsync tidak pernah dipanggil. Saya jalankan dotnet test: merah karena tidak ada exception. Green: satu baris if (request.Price <= 0) throw new ValidationException(...). Setelah aturan Stock dan nama duplikat menyusul, saya refactor: ketiga if diekstrak ke ValidateAsync, lalu test dijalankan lagi dan tetap hijau."

Checklist:

  • nama test tiga bagian: Method_Scenario_ExpectedResult
  • [Theory] + [InlineData] untuk 0 dan angka negatif
  • Arrange (mock repository + request), Act (satu panggilan CreateAsync), Assert (ThrowsAsync)
  • Verify bahwa AddAsync tidak dipanggil (Times.Never)
  • menyebut test dijalankan dan merah sebelum menulis kode green
  • refactor konkret (ekstrak ValidateAsync) dengan test tetap hijau
Bacaan utama (±10 menit) Microsoft Learn: Unit testing best practices for .NET. Baca bagian "Follow test naming standards", "Arrange your tests", dan "Avoid multiple Act tasks". Untuk siklusnya, lanjut ke Kent Beck: Canon TDD (pendek, daftar kesalahan per langkah).

Ringkasan untuk dicetak: Reference: Red-Green-Refactor · Istilah: GLOSSARY.md

Ada yang belum jelas? Tanyakan ke saya (agent) di terminal, misalnya: "kalau refactor itu opsional menurut Beck, kapan saya boleh melewatinya?" atau "apa bedanya mock dan stub di test tadi?" (yang terakhir dibahas di lesson 0010).

Sumber: [1] Microsoft Learn: Unit testing best practices for .NET · [2] Microsoft Learn: Unit testing C# with xUnit · [3] Kent Beck: Canon TDD · [4] Martin Fowler: Test Driven Development · [5] Jest: test.each · [6] Martin Fowler: Self Testing Code