Unit test adalah pemeriksaan terkecil dalam test pyramid. Sebuah unit test memverifikasi perilaku satu fungsi atau modul kecil secara terisolasi—tanpa jaringan, file system, atau modul eksternal. Pada chapter ini kita menulis unit test untuk fungsi add(int, int) menggunakan assert() dari header <assert.h>, lalu memisahkan deklarasi (math.h) dari implementasi (math.c) agar test dapat men-link modul yang diuji tanpa menyertakan kode produksi secara langsung.

Anatomi Unit Test dengan assert() #

Header standar <assert.h> menyediakan macro assert(expr) yang memeriksa kebenaran ekspresi expr. Jika expr bernilai 0 (false), assert akan mencetak pesan ke stderr dan memanggil abort(), menghentikan program dengan kode exit non-zero. Perilaku inilah yang membuat assert cocok untuk unit test: setiap kegagalan langsung terlihat dan menghentikan test runner.

c
/* File: math.h */
#ifndef MATH_H
#define MATH_H

int add(int a, int b);

#endif /* MATH_H */
c
/* File: math.c */
#include "math.h"

int add(int a, int b) {
    return a + b;
}
c
/* File: test_math.c — unit test untuk add() menggunakan assert() */
#include <assert.h>
#include <stdio.h>
#include <limits.h>
#include "math.h"

static int tests_run = 0;

static void test_add_normal(void) {
    tests_run++;
    assert(add(2, 3) == 5);
    printf("[OK] test_add_normal: add(2, 3) = %d\n", add(2, 3));
}

static void test_add_negative(void) {
    tests_run++;
    assert(add(-10, -5) == -15);
    printf("[OK] test_add_negative: add(-10, -5) = %d\n", add(-10, -5));
}

static void test_add_zero(void) {
    tests_run++;
    assert(add(0, 0) == 0);
    assert(add(42, 0) == 42);
    assert(add(0, 99) == 99);
    printf("[OK] test_add_zero: identitas nol\n");
}

static void test_add_overflow_boundary(void) {
    tests_run++;
    /* Kasus batas — bukan overflow, hanya batas aman */
    assert(add(INT_MAX, 0) == INT_MAX);
    assert(add(INT_MIN, 0) == INT_MIN);
    printf("[OK] test_add_overflow_boundary: INT_MAX/INT_MIN + 0\n");
}

int main(void) {
    test_add_normal();
    test_add_negative();
    test_add_zero();
    test_add_overflow_boundary();

    printf("\nSemua %d test lulus.\n", tests_run);
    return 0;
}

Build dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -o test_math test_math.c math.c && ./test_math

Output yang Dihasilkan:

text
[OK] test_add_normal: add(2, 3) = 5
[OK] test_add_negative: add(-10, -5) = -15
[OK] test_add_zero: identitas nol
[OK] test_add_overflow_boundary: INT_MAX/INT_MIN + 0

Semua 4 test lulus.

Mari kita telaah baris-baris kode di atas:

  1. Baris 1 (#include <assert.h>): Menyediakan macro assert. Saat NDEBUG didefinisikan (misalnya dengan flag -DNDEBUG saat compile), assert menjadi no-op—berbahaya di produksi, berguna untuk test.
  2. Baris 3 (#include <limits.h>): Memberi akses ke konstanta INT_MAX dan INT_MIN sehingga test batas tidak bergantung pada nilai hardcoded yang bisa berubah antar platform.
  3. Baris 5 (static int tests_run = 0;): Penghitung global statik melacak jumlah test yang dijalankan. Modifier static membatasi visibilitas hanya pada file ini.
  4. Baris 7-12 (test_add_normal): Setiap test adalah fungsi static void. Pola ini memudahkan test runner memanggil satu-persatu dan melaporkan kegagalan secara individual. tests_run++ diletakkan di awal agar tetap dihitung meski assert gagal.
  5. Baris 30 (assert(add(INT_MAX, 0) == INT_MAX);): Memverifikasi bahwa menambahkan 0 ke INT_MAX tidak mengubah nilai. Kita tidak menguji add(INT_MAX, 1) karena itu signed integer overflow yang merupakan undefined behavior dan tidak boleh diasumsikan hasilnya.
  6. Baris 37-43 (main): Memanggil semua test secara berurutan dan mencetak ringkasan. Bila salah satu assert gagal, program dibatalkan sebelum baris ringkasan tercetak—ini sinyal jelas untuk CI.

Mengapa assert() Cocok untuk Test #

Macro assert memberikan tiga keunggulan praktis untuk unit test:

  • Sintaks ringkas: Cukup tulis assert(expr)—tidak perlu if/else manual untuk setiap kasus.
  • Hentian paksa pada kegagalan: abort() dipanggil saat expr bernilai 0, mencegah test berikutnya dieksekusi dengan kondisi rusak.
  • Lokasi otomatis: Pesan kegagalan mencantumkan nama file dan nomor baris lewat __FILE__ dan __LINE__, memudahkan debugging.

Jebakan Signed Overflow #

Banyak programmer mencoba menguji add(INT_MAX, 1) dan mengharapkan hasil tertentu (misalnya INT_MIN karena wrap-around). Pada kenyataannya, signed integer overflow adalah undefined behavior di C. Compiler bebas mengasumsikan overflow tidak pernah terjadi dan dapat mengoptimalkan kode dengan cara yang mengejutkan. Berikut program yang sama-sama memanggil add(INT_MAX, 1) di bawah -fsanitize=undefined:

c
/* File: test_overflow.c — demonstrasi signed overflow (UB) */
#include <stdio.h>
#include <limits.h>
#include "math.h"

int main(void) {
    int r = add(INT_MAX, 1);
    printf("add(INT_MAX, 1) = %d (UB: tidak boleh diaudit dengan assert)\n", r);
    return 0;
}

Build dengan UBSan:

bash
gcc -std=c11 -Wall -Wextra -fsanitize=undefined \
    -o test_overflow test_overflow.c math.c && ./test_overflow

Output yang Dihasilkan:

text
math.c:4:14: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
add(INT_MAX, 1) = -2147483648 (UB: tidak boleh diaudit dengan assert)

UBSan mendeteksi bahwa 2147483647 + 1 tidak dapat direpresentasikan dalam int. Nilai -2147483648 yang muncul adalah perilaku implementasi, bukan jaminan standar. Pelajaran untuk unit test: jangan pernah menulis assert yang mengandalkan undefined behavior—gunakan precondition check di kode produksi (misalnya if (b > 0 && a > INT_MAX - b) return ERROR;) dan uji jalur error tersebut, bukan jalur UB-nya.

Struktur File Unit Test yang Sehat #

Untuk modul kecil, kita dapat menyusun file sebagai berikut:

  • math.h: Deklarasi publik (prototype).
  • math.c: Implementasi.
  • test_math.c: Test runner yang men-link math.c (atau object file math.o).
  • Makefile (atau test_automation chapter): Target make test meng-compile semua test dan menjalankannya.

Pemisahan ini membuat test tidak tercampur dengan kode produksi. Saat kita mengirim binary ke pengguna, hanya math.o yang ter-link—tidak ada test runner di binary akhir.