Kode C berjalan sangat dekat dengan mesin. Sebuah off-by-one pada indeks array, pembagian dengan nol yang luput dipikirkan, atau dangling pointer dapat menghancurkan seluruh program dalam satu detik. Testing adalah praktik yang memastikan setiap unit perilaku tetap benar selama kode berkembang. Pada chapter ini kita akan melihat bagaimana sebuah test kecil bisa menangkap bug yang sering luput dari inspeksi manual, lalu memetakan jenis-jenis test yang akan dibahas di sepanjang section ini.

Bug yang Luput dari Inspeksi Manual #

Bayangkan kita menulis fungsi average() sederhana yang menghitung rata-rata array int. Saat diuji dengan input {10, 20, 30} fungsi tampak bekerja dengan benar. Namun ketika pemanggil lupa memeriksa apakah count bisa 0, terjadi pembagian dengan nol yang menumbangkan program. Berikut dua versi implementasi: versi buggy dan versi safe.

c
/* File: stats.c — modul statistik sederhana */
#include <stddef.h>

/* Versi BUG: rata-rata salah saat count==0 (divide by zero) */
int average_buggy(const int *data, size_t count) {
    int sum = 0;
    for (size_t i = 0; i < count; i++) {
        sum += data[i];
    }
    return sum / (int)count;  /* BUG: count bisa 0 */
}

/* Versi BENAR: kembalikan 0 dan tangani count==0 */
int average_safe(const int *data, size_t count) {
    if (count == 0) {
        return 0;
    }
    int sum = 0;
    for (size_t i = 0; i < count; i++) {
        sum += data[i];
    }
    return sum / (int)count;
}

Saat kode produksi memanggil average_buggy(data, 0) tanpa perlindungan, runtime mengeluarkan sinyal SIGFPE (Floating Point Exception) dan program berhenti paksa. Berikut hasil eksekusi aktual:

c
int main(void) {
    int data[] = {1, 2, 3};
    printf("average_buggy(data, 3) = %d\n", average_buggy(data, 3));
    fflush(stdout);
    int r = average_buggy(data, 0);  /* BUG: divide by zero */
    printf("average_buggy(data, 0) = %d\n", r);
    return 0;
}

Output yang Dihasilkan:

text
average_buggy(data, 3) = 2
Floating point exception (core dumped)

Baris kedua tidak pernah tercetak. Program dihentikan oleh sinyal SIGFPE sejak sum / (int)count dievaluasi saat count == 0. Inspeksi manual sering melewatkan skenario ini karena pikiran kita terfokus pada kasus “normal” count >= 1.

Test Sederhana yang Menangkap Bug #

Test berikut menguji dua hal sekaligus: kasus normal dan kasus boundary count == 0. Test menggunakan macro CHECK sederhana yang mencatat jumlah kegagalan. Test harness seperti ini cukup untuk memvalidasi perilaku krusial tanpa framework eksternal.

c
/* File: test_stats.c — harness test tanpa framework */
#include <stdio.h>
#include "stats.c"

static int tests_run = 0;
static int tests_failed = 0;

#define CHECK(cond)                                                  \
    do {                                                             \
        tests_run++;                                                 \
        if (!(cond)) {                                               \
            tests_failed++;                                          \
            printf("FAIL: %s (line %d)\n", #cond, __LINE__);          \
        }                                                            \
    } while (0)

int main(void) {
    int data[] = {10, 20, 30};

    /* Kasus normal */
    CHECK(average_safe(data, 3) == 20);

    /* Kasus boundary: count==0 — versi buggy akan crash */
    CHECK(average_safe(data, 0) == 0);

    printf("Total: %d, Failed: %d\n", tests_run, tests_failed);
    return tests_failed != 0;
}

Output yang Dihasilkan:

text
Total: 2, Failed: 0

Mari kita telaah baris-baris kode di atas:

  1. Baris 4 (#include "stats.c"): Kita langsung menyertakan file implementasi. Pendekatan ini umum untuk small static library test. Pada proyek besar, kita biasanya men-link object file, bukan menyertakan source.
  2. Baris 6-7 (static int tests_run = 0; ...): Penghitung global statik melacak berapa test dijalankan dan berapa yang gagal. Variabel static membatasi visibilitas hanya ke file ini.
  3. Baris 9-16 (#define CHECK(cond) ...): Macro CHECK menerima kondisi boolean. Macro ini menggunakan #cond untuk “stringifikasi” kondisi menjadi teks sehingga pesan kegagalan menampilkan ekspresi yang gagal. __LINE__ mencatat lokasi kegagalan.
  4. Baris 22 (CHECK(average_safe(data, 3) == 20);): Memverifikasi kasus normal. Jika implementasi berubah dan rata-rata tidak lagi 20, test gagal dengan pesan FAIL: average_safe(data, 3) == 20 (line 22).
  5. Baris 25 (CHECK(average_safe(data, 0) == 0);): Memverifikasi kasus boundary yang akan membuat versi buggy average_buggy crash dengan SIGFPE. Versi safe mengembalikan 0 tanpa membahayakan program.
  6. Baris 28 (return tests_failed != 0;): main mengembalikan kode exit non-zero bila ada kegagalan. Konvensi ini penting agar continuous integration dapat mendeteksi kegagalan melalui kode exit.

Benefit Jangka Panjang Testing #

Test yang ditulis sekali terus bekerja setiap kali kode berubah. Ketika kita menambah fitur baru atau melakukan refactor, menjalankan test memberi sinyal langsung apakah perubahan merusak perilaku lama. Beberapa manfaat utama yang kita peroleh:

  • Mendeteksi regresi dini: Bug yang pernah diperbaiki tidak akan kembali karena dipantau oleh regression test.
  • Dokumentasi yang hidup: Test yang membaca jelas menyatakan kontrak fungsi—apa yang diharapkan dari input dan output.
  • Refactor tanpa takut: Kita bisa mengganti implementasi internal tanpa khawatir merusak perilaku yang sudah dijamin test.
  • Desain yang lebih baik: Fungsi yang sulit diuji biasanya berdesain buruk—terlalu banyak side effect atau dependensi. Test memaksa kita menulis fungsi yang composable.

Peta Section Ini #

Chapter-chapter selanjutnya akan membahas topik testing secara berurutan: dari unit testing dan integration testing, ke alur test-driven development, kemudian framework-framework C seperti Unity, CMocka, dan Criterion. Setelah itu kita membangun test harness sendiri, lalu membahas assertions, boundary testing, property-based testing, fuzz testing, regression testing, code coverage dengan gcov, sanitizers (-fsanitize=address,undefined), dan diakhiri dengan test automation melalui Makefile dan CI. Setiap topik saling melengkapi membentuk praktik quality assurance yang menyeluruh untuk proyek C.