Menulis test dari nol dengan assert() cepat berantakan ketika jumlah test bertambah: pelaporan kegagalan tidak konsisten, tidak ada penghitung otomatis, dan setup/teardown berubah menjadi boilerplate. Test framework hadir untuk menyelesaikan masalah ini. Tiga framework populer di ekosistem C adalah Unity (satu file, sangat ringan, dipakai di proyek embedded), CMocka (mendukung mock object), dan Criterion (sintaks modern dengan auto-registrasi test). Pada chapter ini kita memandang ketiganya secara konseptual, lalu membangun mini-framework yang meniru API Unity untuk demonstrasi tanpa dependensi eksternal.
Lanskap Framework C #
| Framework | Ukuran | Fitur Khas | Cocok Untuk |
|---|---|---|---|
| Unity | ~1 file .c | Assert macros (TEST_ASSERT_EQUAL_INT, TEST_ASSERT_EQUAL_STRING), RUN_TEST | Embedded, firmware |
| CMocka | Library dinamis | Mock function, check_expected(), will_return() | Modul dengan banyak dependensi |
| Criterion | Library + header teori | Auto-registrasi Test(name, tc), parameterized test, TAP output | Aplikasi besar, CI |
| Check | Library + fork | Fixture, fork isolation per test | Proyek desktop dengan dependensi |
Pilihan framework memengaruhi cara kita menulis test. Unity meminjam pola RUN_TEST(nama_fungsi)—test adalah fungsi global yang dipanggil secara eksplisit. Criterion memakai macro Test(suite, name) yang melakukan auto-registrasi pada main yang di-generate. CMocka berada di tengah—memakai cmocka_run_group_tests dengan array struktur test.
Struktur API Unity yang Ditiru #
Unity memusatkan pengujian pada empat konsep: penghitung global (UNITY_TESTS_RUN, UNITY_TESTS_FAIL), macro UNITY_BEGIN()/UNITY_END(), macro RUN_TEST(test), dan family TEST_ASSERT_*. Berikut implementasi minimal yang menyediakan API tersebut:
/* File: mini_unity.h — emulasi minimal Unity API */
#ifndef MINI_UNITY_H
#define MINI_UNITY_H
#include <stdio.h>
#include <string.h>
extern int UNITY_TESTS_RUN;
extern int UNITY_TESTS_FAIL;
extern int UNITY_TESTS_IGNORE;
#define UNITY_BEGIN() do { \
UNITY_TESTS_RUN = 0; UNITY_TESTS_FAIL = 0; UNITY_TESTS_IGNORE = 0; \
} while (0)
#define UNITY_END() (UNITY_TESTS_FAIL > 0 ? 1 : 0)
#define RUN_TEST(test) run_test(#test, test)
typedef void (*UnityTestFunc)(void);
static inline void run_test(const char *name, UnityTestFunc fn) {
UNITY_TESTS_RUN++;
int fail_before = UNITY_TESTS_FAIL;
printf("RUN %s ... ", name);
fflush(stdout);
fn();
if (UNITY_TESTS_FAIL > fail_before) {
/* Pesan FAIL sudah dicetak oleh macro TEST_ASSERT_* */
} else {
printf("PASS\n");
}
}
#define TEST_ASSERT_EQUAL_INT(expected, actual) \
do { \
int _e = (expected); int _a = (actual); \
if (_e != _a) { \
printf("FAIL (line %d): expected %d, got %d\n", __LINE__, _e, _a); \
UNITY_TESTS_FAIL++; \
return; \
} \
} while (0)
#define TEST_ASSERT_TRUE(cond) \
do { \
if (!(cond)) { \
printf("FAIL (line %d): expected true, got false\n", __LINE__); \
UNITY_TESTS_FAIL++; \
return; \
} \
} while (0)
void setUp(void);
void tearDown(void);
#endif /* MINI_UNITY_H *//* File: mini_unity.c — definisi penghitung global + setUp/tearDown default */
#include "mini_unity.h"
int UNITY_TESTS_RUN = 0;
int UNITY_TESTS_FAIL = 0;
int UNITY_TESTS_IGNORE = 0;
__attribute__((weak)) void setUp(void) {}
__attribute__((weak)) void tearDown(void) {}Implementasi setUp/tearDown menggunakan atribut weak sehingga file test boleh, tetapi tidak wajib, mendefinisikan ulang kedua fungsi tersebut.
Test File Menggunakan mini_unity #
Berikut test untuk modul strlib yang menyediakan my_strlen(). Test memverifikasi kasus string normal, string kosong, parameter NULL, dan string panjang—empat skenario yang sering luput di test naif.
/* File: strlib.h */
#ifndef STRLIB_H
#define STRLIB_H
#include <stddef.h>
size_t my_strlen(const char *s);
#endif/* File: strlib.c */
#include "strlib.h"
#include <stddef.h>
size_t my_strlen(const char *s) {
if (s == NULL) return 0;
size_t n = 0;
while (s[n] != '\0') n++;
return n;
}/* File: test_strlib.c — test menggunakan mini_unity */
#include "mini_unity.h"
#include "strlib.h"
#include <stddef.h>
void setUp(void) {
/* Persiapan sebelum setiap test (kosong di sini) */
}
void tearDown(void) {
/* Pembersihan setelah setiap test (kosong di sini) */
}
static void test_strlen_normal(void) {
TEST_ASSERT_EQUAL_INT(5, (int)my_strlen("halo!"));
}
static void test_strlen_empty(void) {
TEST_ASSERT_EQUAL_INT(0, (int)my_strlen(""));
}
static void test_strlen_null_safe(void) {
/* my_strlen(NULL) harus mengembalikan 0, bukan crash */
TEST_ASSERT_EQUAL_INT(0, (int)my_strlen(NULL));
}
static void test_strlen_long(void) {
TEST_ASSERT_EQUAL_INT(13, (int)my_strlen("belajar_c_tdd"));
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_strlen_normal);
RUN_TEST(test_strlen_empty);
RUN_TEST(test_strlen_null_safe);
RUN_TEST(test_strlen_long);
int rc = UNITY_END();
printf("\nTotal: %d, Failed: %d, Ignored: %d\n",
UNITY_TESTS_RUN, UNITY_TESTS_FAIL, UNITY_TESTS_IGNORE);
return rc;
}Build dan jalankan:
gcc -std=c11 -Wall -Wextra -o test_strlib \
test_strlib.c strlib.c mini_unity.c && ./test_strlibOutput yang Dihasilkan:
RUN test_strlen_normal ... PASS
RUN test_strlen_empty ... PASS
RUN test_strlen_null_safe ... PASS
RUN test_strlen_long ... PASS
Total: 4, Failed: 0, Ignored: 0Pelaporan Kegagalan #
Saat sebuah test gagal, macro TEST_ASSERT_* mencetak pesan yang mencantumkan nomor baris dan nilai aktual versus yang diharapkan. Kegagalan tidak menghentikan seluruh test runner—hanya test yang sedang berjalan. Berikut demo test yang sengaja salah:
/* File: test_fail.c — demonstrasi test yang gagal */
#include "mini_unity.h"
#include "strlib.h"
#include <stddef.h>
void setUp(void) {}
void tearDown(void) {}
static void test_wrong_length(void) {
/* Sengaja salah — my_strlen("abc") == 3, bukan 4 */
TEST_ASSERT_EQUAL_INT(4, (int)my_strlen("abc"));
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_wrong_length);
int rc = UNITY_END();
printf("\nTotal: %d, Failed: %d\n", UNITY_TESTS_RUN, UNITY_TESTS_FAIL);
return rc;
}Output yang Dihasilkan:
RUN test_wrong_length ... FAIL (line 10): expected 4, got 3
Total: 1, Failed: 1Mari kita telaah baris-baris kunci di mini_unity:
run_test(filemini_unity.h): Fungsi ini menyimpanUNITY_TESTS_FAILsebelum test dijalankan (fail_before). Setelah test kembali, fungsi membandingkanUNITY_TESTS_FAILsaat ini denganfail_before. Bila bertambah, pesanFAILsudah dicetak oleh macro di dalam test—kita tidak boleh mencetakPASS.TEST_ASSERT_EQUAL_INT(expected, actual): Macro melakukan cast keint, membandingkan nilai, lalu—bila tidak sama—mencetak baris kegagalan, menambahUNITY_TESTS_FAIL, danreturn. Penggunaanreturndi tengah fungsivoidadalah trik yang lazim di framework C agar test gagal langsung berhenti tanpasetjmp/longjmp.UNITY_END(): Mengembalikan1bila ada kegagalan,0bila semua lulus.mainmengembalikan nilai ini sebagai kode exit agar CI dapat menentukan keberhasilan.
Memilih Framework untuk Proyek C #
- Embedded / single-binary kecil: Pilih Unity. Satu file
.cmudah disertakan langsung di build system. Unity juga menyediakansetUp/tearDownotomatis yang dipanggil di sekitar setiap test. - Mocking penting: Pilih CMocka. CMocka menyediakan
mock()yang mengembalikan nilai yang ditentukan olehwill_return()—memungkinkan test fungsi yang memanggil dependency eksternal tanpa dependency sungguhan. - CI dengan banyak test: Pilih Criterion. Auto-registrasi
Test(suite, tc) { ... }menghapus boilerplateRUN_TEST. Output dapat diatur ke TAP, JSON, atau plain text—cocok untuk integrasi dengan CI dashboard.