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.
/* File: math.h */
#ifndef MATH_H
#define MATH_H
int add(int a, int b);
#endif /* MATH_H *//* File: math.c */
#include "math.h"
int add(int a, int b) {
return a + b;
}/* 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:
gcc -std=c11 -Wall -Wextra -o test_math test_math.c math.c && ./test_mathOutput yang Dihasilkan:
[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:
- Baris 1 (
#include <assert.h>): Menyediakan macroassert. SaatNDEBUGdidefinisikan (misalnya dengan flag-DNDEBUGsaat compile),assertmenjadi no-op—berbahaya di produksi, berguna untuk test. - Baris 3 (
#include <limits.h>): Memberi akses ke konstantaINT_MAXdanINT_MINsehingga test batas tidak bergantung pada nilai hardcoded yang bisa berubah antar platform. - Baris 5 (
static int tests_run = 0;): Penghitung global statik melacak jumlah test yang dijalankan. Modifierstaticmembatasi visibilitas hanya pada file ini. - Baris 7-12 (
test_add_normal): Setiap test adalah fungsistatic void. Pola ini memudahkan test runner memanggil satu-persatu dan melaporkan kegagalan secara individual.tests_run++diletakkan di awal agar tetap dihitung meskiassertgagal. - Baris 30 (
assert(add(INT_MAX, 0) == INT_MAX);): Memverifikasi bahwa menambahkan0keINT_MAXtidak mengubah nilai. Kita tidak mengujiadd(INT_MAX, 1)karena itu signed integer overflow yang merupakan undefined behavior dan tidak boleh diasumsikan hasilnya. - Baris 37-43 (
main): Memanggil semua test secara berurutan dan mencetak ringkasan. Bila salah satuassertgagal, 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 perluif/elsemanual untuk setiap kasus. - Hentian paksa pada kegagalan:
abort()dipanggil saatexprbernilai0, 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:
/* 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:
gcc -std=c11 -Wall -Wextra -fsanitize=undefined \
-o test_overflow test_overflow.c math.c && ./test_overflowOutput yang Dihasilkan:
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-linkmath.c(atau object filemath.o).Makefile(atautest_automationchapter): Targetmake testmeng-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.