Red-Green-Refactor pada satu aturan Product
Siklusnya, kenapa harus merah dulu, Arrange-Act-Assert, dan nama test yang bisa dibaca
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]
| Langkah | Yang Anda lakukan | Kesalahan yang disebut Beck [3] | Padanan Node |
|---|---|---|---|
| Red | "Turn exactly one item on the list into an actual, concrete, runnable test", jalankan, lihat gagal | Mengubah semua item list menjadi test sekaligus; test tanpa assertion | Satu 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]
- Merah membuktikan test itu peka terhadap perilaku yang hendak Anda tambahkan. Hijau sesudahnya baru berarti sesuatu.
- Merah harus karena alasan yang benar.
NullReferenceExceptiondari setup yang rusak, atau error compile, bukan bukti apa pun tentang aturan harga. Perbaiki test-nya dulu. - Untuk bug, urutannya sama. Fowler: tim yang sehat akan "write a test that exposes the bug, and only then to try to fix it." [6]
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]
| Konvensi | xUnit (.NET) | Padanan Jest |
|---|---|---|
| Nama test: method, skenario, hasil [1] | CreateAsync_PriceNotPositive_ThrowsValidationException | describe("createProduct") + it("throws when price ≤ 0") |
| Satu kasus | [Fact] | test() / it() |
| Kode sama, input berbeda [2] | [Theory] + [InlineData(0)] | test.each [5] |
| Menjalankan terus-menerus | dotnet watch test | jest --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.
- Ambil satu item dari test list: harga ≤ 0 ditolak
- Tulis
CreateAsync_PriceNotPositive_ThrowsValidationExceptiondengan Arrange-Act-Assert - Jalankan
dotnet test: merah, karena tidak ada exception yang dilempar - Tambahkan satu
if (request.Price <= 0) throwdiCreateAsync - Jalankan
dotnet test: test ini dan semua test lama hijau - Ekstrak validasi ke
ValidateAsync, jalankan test lagi: tetap hijau - 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) VerifybahwaAddAsynctidak dipanggil (Times.Never)- menyebut test dijalankan dan merah sebelum menulis kode green
- refactor konkret (ekstrak
ValidateAsync) dengan test tetap hijau
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