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 #

FrameworkUkuranFitur KhasCocok Untuk
Unity~1 file .cAssert macros (TEST_ASSERT_EQUAL_INT, TEST_ASSERT_EQUAL_STRING), RUN_TESTEmbedded, firmware
CMockaLibrary dinamisMock function, check_expected(), will_return()Modul dengan banyak dependensi
CriterionLibrary + header teoriAuto-registrasi Test(name, tc), parameterized test, TAP outputAplikasi besar, CI
CheckLibrary + forkFixture, fork isolation per testProyek 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:

c
/* 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 */
c
/* 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.

c
/* File: strlib.h */
#ifndef STRLIB_H
#define STRLIB_H
#include <stddef.h>
size_t my_strlen(const char *s);
#endif
c
/* 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;
}
c
/* 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:

bash
gcc -std=c11 -Wall -Wextra -o test_strlib \
    test_strlib.c strlib.c mini_unity.c && ./test_strlib

Output yang Dihasilkan:

text
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: 0

Pelaporan 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:

c
/* 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:

text
RUN  test_wrong_length ... FAIL (line 10): expected 4, got 3

Total: 1, Failed: 1

Mari kita telaah baris-baris kunci di mini_unity:

  1. run_test (file mini_unity.h): Fungsi ini menyimpan UNITY_TESTS_FAIL sebelum test dijalankan (fail_before). Setelah test kembali, fungsi membandingkan UNITY_TESTS_FAIL saat ini dengan fail_before. Bila bertambah, pesan FAIL sudah dicetak oleh macro di dalam test—kita tidak boleh mencetak PASS.
  2. TEST_ASSERT_EQUAL_INT(expected, actual): Macro melakukan cast ke int, membandingkan nilai, lalu—bila tidak sama—mencetak baris kegagalan, menambah UNITY_TESTS_FAIL, dan return. Penggunaan return di tengah fungsi void adalah trik yang lazim di framework C agar test gagal langsung berhenti tanpa setjmp/longjmp.
  3. UNITY_END(): Mengembalikan 1 bila ada kegagalan, 0 bila semua lulus. main mengembalikan nilai ini sebagai kode exit agar CI dapat menentukan keberhasilan.

Memilih Framework untuk Proyek C #

  • Embedded / single-binary kecil: Pilih Unity. Satu file .c mudah disertakan langsung di build system. Unity juga menyediakan setUp/tearDown otomatis yang dipanggil di sekitar setiap test.
  • Mocking penting: Pilih CMocka. CMocka menyediakan mock() yang mengembalikan nilai yang ditentukan oleh will_return()—memungkinkan test fungsi yang memanggil dependency eksternal tanpa dependency sungguhan.
  • CI dengan banyak test: Pilih Criterion. Auto-registrasi Test(suite, tc) { ... } menghapus boilerplate RUN_TEST. Output dapat diatur ke TAP, JSON, atau plain text—cocok untuk integrasi dengan CI dashboard.