Test-Driven Development (TDD) adalah alur kerja di mana kita menulis test sebelum kode produksi. Pembalikan urutan ini terasa janggal pada awalnya, tetapi memaksa kita memikirkan kontrak fungsi terlebih dahulu: apa input yang diterima, apa output yang diharapkan, dan apa edge case yang harus ditangani. TDD di C mengikuti siklus tiga langkah yang dikenal sebagai Red-Green-Refactor: tulis test yang gagal (Red), tulis implementasi minimal supaya test lulus (Green), lalu perbaiki struktur kode tanpa mengubah perilaku (Refactor). Pada chapter ini kita mempraktikkan ketiga langkah tersebut untuk fungsi is_even(int).

Siklus Red-Green-Refactor #

Siklus dimulai dari test yang gagal karena kode produksi belum ada. Setelah implementasi minimal membuat test lulus, kita bebas memperbaiki kualitas kode (rename, ekstrak fungsi, ganti algoritma) selama test tetap hijau. Setiap iterasi siklus menambahkan satu perilaku ke sistem.

Langkah 1 — Red: Tulis Test yang Gagal #

Pada langkah Red, kita menulis test untuk fungsi is_even(int n) yang mengembalikan 1 bila n genap dan 0 bila ganjil. Fungsi tersebut belum diimplementasikan, jadi compiler akan menolak me-link program.

c
/* TDD Step 1 (RED): test untuk is_even() yang belum diimplementasikan */
#include <assert.h>
#include <stdio.h>

extern int is_even(int n);

int main(void) {
    assert(is_even(2) == 1);
    assert(is_even(3) == 0);
    assert(is_even(0) == 1);
    assert(is_even(-4) == 1);
    printf("Step 1 (RED): test written, akan gagal compile\n");
    return 0;
}

Build dengan:

bash
gcc -std=c11 -Wall -Wextra -o step1_red step1_red.c

Output yang Dihasilkan:

text
/usr/bin/ld: /tmp/ccXXXXXX.o: in function `main':
step1_red.c:(.text+0xa): undefined reference to `is_even'
step1_red.c:(.text+0x41): undefined reference to `is_even'
step1_red.c:(.text+0x77): undefined reference to `is_even'
step1_red.c:(.text+0xae): undefined reference to `is_even'
collect2: error: ld returned 1 exit status

Linker melaporkan undefined reference karena simbol is_even belum ada. Inilah definisi “Red” dalam TDD: test berstatus gagal, dan kegagalan tersebut bersifat informative—memberi tahu kita bahwa langkah berikutnya adalah menulis implementasi.

Langkah 2 — Green: Implementasi Minimal #

Setelah test Red, kita menulis implementasi sesederhana mungkin agar test lulus. Tidak boleh ada optimisasi prematur atau fitur tambahan—cukup buat test hijau.

c
/* TDD Step 2 (GREEN): implementasi minimal agar test lulus */
#include <assert.h>
#include <stdio.h>

int is_even(int n) {
    return n % 2 == 0;
}

int main(void) {
    assert(is_even(2) == 1);
    assert(is_even(3) == 0);
    assert(is_even(0) == 1);
    assert(is_even(-4) == 1);
    printf("Step 2 (GREEN): semua test lulus\n");
    return 0;
}

Output yang Dihasilkan:

text
Step 2 (GREEN): semua test lulus

Mari kita telaah baris-baris kode di atas:

  1. Baris 6 (int is_even(int n) {): Implementasi pertama is_even. Pemilihan operator modulo % cukup karena % 2 didefinisikan untuk semua int, termasuk bilangan negatif.
  2. Baris 7 (return n % 2 == 0;): Ekspresi n % 2 == 0 menghasilkan 1 bila n genap dan 0 bila ganjil. Tidak ada cabang if—kita memilih ekspresi boolean langsung agar kode lebih ringkas.
  3. Baris 11-14 (assert(...)): Empat test memverifikasi kasus positif (2), negatif ganjil (3), nol (0), dan negatif genap (-4). Empat kasus ini melingkupi jalur logika utama.
  4. Baris 15 (printf("Step 2 (GREEN)...\n");): Pesan tercetak hanya bila semua assert lulus. Bila ada kegagalan, assert memanggil abort() dan pesan tidak tercetak—sinyal kuat untuk CI.

Langkah 3 — Refactor: Tingkatkan Kualitas Tanpa Mengubah Perilaku #

Pada langkah Refactor, kita memperbaiki struktur kode tanpa mengubah perilaku. Test yang sudah hijau menjadi pengaman: setiap refactor yang salah akan langsung ditangkap. Kita mengelompokkan test ke dalam fungsi terpisah agar lebih mudah dibaca, dan mengganti operator modulo dengan operasi bitwise—kedua pendekatan menghasilkan hasil yang sama untuk bilangan bulat.

c
/* TDD Step 3 (REFACTOR): pisahkan implementasi dan test ke file terpisah */
#include <assert.h>
#include <stdio.h>

/* Implementasi setara, dipindah ke blok terpisah untuk kejelasan */
int is_even(int n) {
    /* Gunakan bitwise AND — ekuivalen dengan n % 2 == 0 untuk bilangan bulat */
    return (n & 1) == 0;
}

static void test_is_even_positive(void) {
    assert(is_even(2) == 1);
    assert(is_even(4) == 1);
    assert(is_even(100) == 1);
}

static void test_is_even_odd(void) {
    assert(is_even(1) == 0);
    assert(is_even(3) == 0);
    assert(is_even(99) == 0);
}

static void test_is_even_edge(void) {
    assert(is_even(0) == 1);   /* nol dianggap genap */
    assert(is_even(-2) == 1); /* bilangan negatif genap */
    assert(is_even(-1) == 0); /* bilangan negatif ganjil */
}

int main(void) {
    test_is_even_positive();
    test_is_even_odd();
    test_is_even_edge();
    printf("Step 3 (REFACTOR): %d test terstruktur lulus\n", 9);
    return 0;
}

Output yang Dihasilkan:

text
Step 3 (REFACTOR): 9 test terstruktur lulus

Refactor mengganti n % 2 == 0 dengan (n & 1) == 0. Kedua ekspresi menghasilkan perilaku identik untuk int. Test yang sudah hijau tetap lulus setelah perubahan, membuktikan bahwa refactor tidak mengubah kontrak fungsi.

Kapan TDD Memberi Nilai, Kapan Tidak #

TDD paling berharga saat kita menulis library function dengan kontrak jelas—parser, data structure, algorithm. TDD kurang cocok untuk:

  • Spike eksplorasi: Kode sekali pakai yang akan dibuang sebaiknya tidak dibebani test terlebih dahulu.
  • GUI dan interaksi pengguna: Lebih efektif diuji dengan integration test atau manual testing.
  • Penelitian algoritma baru: Saat algoritma belum stabil, test prematur bisa mengunci desain yang salah.