Test framework eksternal kadang tidak tersedia di target platform—embedded, firmware, atau environment air-gapped. Pada situasi seperti ini kita dapat membangun test harness sendiri: sebuah header kecil berisi macro TEST_ASSERT_*, registry test berupa array TestCase, dan satu fungsi run_tests() yang mencatat keberhasilan dan kegagalan. Membangun harness sendiri juga cara terbaik untuk benar-benar memahami cara kerja framework seperti Unity atau CMocka. Pada chapter ini kita membangun harness dari nol—tanpa library eksternal—dan menggunakannya untuk menguji modul Vec2.
Komponen Utama Test Harness #
Empat komponen yang harus dimiliki setiap harness minimalis:
- Penghitung global (
g_tests_run,g_tests_failed) yang dipakai bersama semua test. - Macro
TEST_ASSERT_*yang mencatat kegagalan dan keluar dari fungsi test denganreturn. - Struktur
TestCaseyang menyimpan pasangan nama string dan pointer fungsi test. - Fungsi
run_tests(const TestCase *tests)yang mengiterasi array dan memanggil setiap test sambil melaporkan hasil.
/* File: harness.h — test runner sederhana dengan array registry */
#ifndef HARNESS_H
#define HARNESS_H
#include <stdio.h>
#include <string.h>
extern int g_tests_run;
extern int g_tests_failed;
typedef void (*TestFn)(void);
typedef struct {
const char *name;
TestFn fn;
} TestCase;
/* Macro TEST_ASSERT_EQUAL_INT — bandingkan dua int */
#define TEST_ASSERT_EQUAL_INT(expected, actual) \
do { \
int _e = (expected); int _a = (actual); \
if (_e != _a) { \
printf(" FAIL: %s:%d: expected %d, got %d\n", \
__FILE__, __LINE__, _e, _a); \
g_tests_failed++; \
return; \
} \
} while (0)
/* Macro TEST_ASSERT_EQUAL_STRING */
#define TEST_ASSERT_EQUAL_STRING(expected, actual) \
do { \
const char *_e = (expected); const char *_a = (actual); \
if (_e == NULL || _a == NULL || strcmp(_e, _a) != 0) { \
printf(" FAIL: %s:%d: string mismatch\n", __FILE__, __LINE__); \
g_tests_failed++; \
return; \
} \
} while (0)
/* Macro TEST_ASSERT_TRUE */
#define TEST_ASSERT_TRUE(cond) \
do { \
if (!(cond)) { \
printf(" FAIL: %s:%d: expected true\n", __FILE__, __LINE__); \
g_tests_failed++; \
return; \
} \
} while (0)
/* Macro untuk definisi test: TEST(test_name) { ... } */
#define TEST(name) static void name(void)
/* Macro untuk mendaftarkan satu test ke array */
#define MAKE_TEST(name) { #name, name }
/* Fungsi untuk menjalankan array test yang diakhiri {NULL, NULL} */
static inline void run_tests(const TestCase *tests) {
for (size_t i = 0; tests[i].fn != NULL; i++) {
int fail_before = g_tests_failed;
g_tests_run++;
printf("[RUN ] %s\n", tests[i].name);
tests[i].fn();
if (g_tests_failed == fail_before) {
printf("[PASS] %s\n", tests[i].name);
}
}
}
#endif /* HARNESS_H *//* File: harness.c — definisi penghitung global */
#include "harness.h"
int g_tests_run = 0;
int g_tests_failed = 0;Modul Produksi yang Diuji #
Modul Vec2 menyimpan vektor 2D dan menyediakan operasi vec_add serta vec_dot. Operasi-operasi ini cukup sederhana untuk diuji menyeluruh, tetapi cukup bermakna untuk memverifikasi bahwa harness kita menangani berbagai tipe data (struct return, scalar return).
/* File: vec.h */
#ifndef VEC_H
#define VEC_H
typedef struct { int x; int y; } Vec2;
Vec2 vec_add(Vec2 a, Vec2 b);
int vec_dot(Vec2 a, Vec2 b);
#endif/* File: vec.c */
#include "vec.h"
Vec2 vec_add(Vec2 a, Vec2 b) {
Vec2 r = { a.x + b.x, a.y + b.y };
return r;
}
int vec_dot(Vec2 a, Vec2 b) {
return a.x * b.x + a.y * b.y;
}Test Runner Menggunakan Harness #
Berikut test runner yang mendaftarkan empat test ke array dan memanggil run_tests. Array diakhiri sentinel {NULL, NULL} sehingga loop berhenti otomatis.
/* File: test_vec.c — test runner dengan harness buatan sendiri */
#include <stdio.h>
#include "harness.h"
#include "vec.h"
TEST(test_vec_add_basic) {
Vec2 a = {1, 2};
Vec2 b = {3, 4};
Vec2 r = vec_add(a, b);
TEST_ASSERT_EQUAL_INT(4, r.x);
TEST_ASSERT_EQUAL_INT(6, r.y);
}
TEST(test_vec_add_zero) {
Vec2 a = {5, 7};
Vec2 zero = {0, 0};
Vec2 r = vec_add(a, zero);
TEST_ASSERT_EQUAL_INT(5, r.x);
TEST_ASSERT_EQUAL_INT(7, r.y);
}
TEST(test_vec_dot) {
Vec2 a = {1, 2};
Vec2 b = {3, 4};
TEST_ASSERT_EQUAL_INT(11, vec_dot(a, b));
}
TEST(test_vec_dot_orthogonal) {
Vec2 a = {1, 0};
Vec2 b = {0, 1};
TEST_ASSERT_EQUAL_INT(0, vec_dot(a, b));
}
int main(void) {
TestCase tests[] = {
MAKE_TEST(test_vec_add_basic),
MAKE_TEST(test_vec_add_zero),
MAKE_TEST(test_vec_dot),
MAKE_TEST(test_vec_dot_orthogonal),
{NULL, NULL}
};
run_tests(tests);
printf("\n=== Hasil ===\n");
printf("Total: %d, Failed: %d\n", g_tests_run, g_tests_failed);
return g_tests_failed > 0 ? 1 : 0;
}Build dan jalankan:
gcc -std=c11 -Wall -Wextra -o test_vec test_vec.c vec.c harness.c && ./test_vecOutput yang Dihasilkan:
[RUN ] test_vec_add_basic
[PASS] test_vec_add_basic
[RUN ] test_vec_add_zero
[PASS] test_vec_add_zero
[RUN ] test_vec_dot
[PASS] test_vec_dot
[RUN ] test_vec_dot_orthogonal
[PASS] test_vec_dot_orthogonal
=== Hasil ===
Total: 4, Failed: 0Mari kita telaah baris-baris kunci di atas:
- Baris
extern int g_tests_run;(fileharness.h): Variabel global dideklarasikanexterndi header agar setiap file yang meng-include header dapat mengakses penghitung. Definisi aktual (tanpaextern) hanya ada diharness.cuntuk menghindari multiple definition. - Macro
TEST(name)(#define TEST(name) static void name(void)): Macro ini mengubahTEST(test_vec_dot)menjadistatic void test_vec_dot(void). Hasilnya: test terlihat seperti fungsi biasa, tetapi didaftarkan lewatMAKE_TEST. - Macro
MAKE_TEST(name) { #name, name }:#nameadalah operator stringifikasi—MAKE_TEST(test_vec_dot)menjadi{ "test_vec_dot", test_vec_dot }. Trik ini menghasilkan arrayTestCaseyang ringkas tanpa duplikasi nama. - Macro
TEST_ASSERT_EQUAL_INT(expected, actual): Menggunakando { ... } while (0)agar macro aman dipakai dalamifsatu baris. Variabel lokal_edan_adiawali underscore untuk menghindari benturan dengan variabel test. Bila gagal, macro mencetak pesan dengan__FILE__dan__LINE__lalureturn—menghentikan test saat ini tanpa menghentikan seluruh runner. - Fungsi
run_tests(fileharness.h): Loop berhenti ketikatests[i].fn == NULL(sentinel). Sebelum setiap test,fail_before = g_tests_faileddisimpan. Setelah test kembali, bilag_tests_failed == fail_before, pesanPASSdicetak. Bila tidak sama, berarti test gagal dan pesanFAILsudah dicetak oleh macro—kita tidak boleh mencetakPASS. - Baris
return g_tests_failed > 0 ? 1 : 0;(filetest_vec.c): Kode exit1bila ada kegagalan. CI seperti GitHub Actions atau Makefile targettestmengandalkan kode exit untuk menentukan keberhasilan build.
Pelaporan Kegagalan #
Saat sebuah test gagal, harness mencatat baris kegagalan dan nama file. Test berikutnya tetap dijalankan—pendekatan continue-on-failure memungkinkan kita melihat semua kegagalan sekaligus, bukan hanya yang pertama.
/* File: test_vec_fail.c — demonstrasi test yang gagal */
#include <stdio.h>
#include "harness.h"
#include "vec.h"
TEST(test_vec_dot_wrong) {
Vec2 a = {1, 2};
Vec2 b = {3, 4};
/* vec_dot({1,2},{3,4}) = 3 + 8 = 11, bukan 12 */
TEST_ASSERT_EQUAL_INT(12, vec_dot(a, b));
}
int main(void) {
TestCase tests[] = {
MAKE_TEST(test_vec_dot_wrong),
{NULL, NULL}
};
run_tests(tests);
printf("\nTotal: %d, Failed: %d\n", g_tests_run, g_tests_failed);
return g_tests_failed > 0 ? 1 : 0;
}Output yang Dihasilkan:
[RUN ] test_vec_dot_wrong
FAIL: test_vec_fail.c:10: expected 12, got 11
Total: 1, Failed: 1Pesan FAIL mencantumkan test_vec_fail.c:10—tepat baris tempat asersi salah berada. Pesan PASS tidak muncul karena g_tests_failed bertambah. Harness mengembalikan kode exit 1 agar Makefile atau CI menandai pipeline sebagai gagal.