Library tanpa test adalah black box yang tidak terpercaya. Pada bagian ini, kita tulis unit test yang memverifikasi setiap fungsi API bekerja sesuai kontrak. Kita pakai pola test runner yang sama dengan proyek 7 dan 8: macro ASSERT yang menghitung pass/fail, dengan edge case yang mencakup input valid, invalid, dan boundary.

Konsep #

Unit test untuk library berbeda dari test aplikasi: kita tidak punya main.c yang dipakai pengguna. Test harus memverifikasi API publik secara langsung. Strategi yang kita pakai:

  1. Satu test function per API function — test_mean, test_stddev, test_reverse_bytes, dst.
  2. Edge case coverage — NULL input, n=0, nilai maksimum, buffer kecil.
  3. Floating point comparison — fabs(actual - expected) < 1e-9 untuk presisi double.
  4. Smoke test version — pastikan exlib_version mengembalikan string yang benar.

Mekanisme kerja: setiap test function inisialisasi state lokal (tidak ada shared state), jalankan beberapa ASSERT, dan return. main() memanggil semua test function secara berurutan, lalu cetak ringkasan. Exit code 0 bila semua lulus, 1 bila ada yang gagal.

Implementasi #

Test runner tests/test_examplelib.c (ringkas):

c
/* test_examplelib.c - Unit test untuk examplelib */
#include <stdio.h>
#include <string.h>
#include <math.h>
#include "examplelib.h"

static int tests_run = 0;
static int tests_pass = 0;

#define ASSERT(cond, msg) \
    do { \
        tests_run++; \
        if (cond) { \
            tests_pass++; \
        } else { \
            printf("FAIL: %s (line %d)\n", msg, __LINE__); \
        } \
    } while (0)

static void test_version(void) {
    ASSERT(strcmp(exlib_version(), "1.0.0") == 0,
           "versi harus 1.0.0");
    ASSERT(strcmp(exlib_strerror(EXLIB_OK), "sukses") == 0,
           "strerror OK = 'sukses'");
    ASSERT(strcmp(exlib_strerror(EXLIB_ERR_NOT_FOUND),
                  "tidak ditemukan") == 0,
           "strerror NOT_FOUND sesuai");
}

static void test_mean(void) {
    double data[] = {1.0, 2.0, 3.0, 4.0, 5.0};
    double mean = 0.0;
    exlib_status s = exlib_mean(data, 5, &mean);
    ASSERT(s == EXLIB_OK, "mean harus sukses");
    ASSERT(fabs(mean - 3.0) < 1e-9, "mean = 3.0");

    s = exlib_mean(NULL, 5, &mean);
    ASSERT(s == EXLIB_ERR_NULL, "mean(NULL) = ERR_NULL");

    s = exlib_mean(data, 0, &mean);
    ASSERT(s == EXLIB_ERR_NULL, "mean n=0 = ERR_NULL");
}

static void test_stddev(void) {
    double data[] = {2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0};
    double sd = 0.0;
    exlib_status s = exlib_stddev(data, 8, &sd);
    ASSERT(s == EXLIB_OK, "stddev harus sukses");
    /* Standar deviasi populasi: sqrt(32/8) = 2.0 */
    ASSERT(fabs(sd - 2.0) < 1e-9, "stddev = 2.0");
}

static void test_reverse_bytes(void) {
    uint8_t data[] = {1, 2, 3, 4, 5};
    exlib_status s = exlib_reverse_bytes(data, 5);
    ASSERT(s == EXLIB_OK, "reverse sukses");
    ASSERT(data[0] == 5 && data[1] == 4 && data[2] == 3,
           "data setelah reverse = 5,4,3,2,1");
    ASSERT(data[3] == 2 && data[4] == 1, "data[3..4] = 2,1");

    s = exlib_reverse_bytes(NULL, 5);
    ASSERT(s == EXLIB_ERR_NULL, "reverse NULL = ERR_NULL");

    uint8_t single[] = {42};
    s = exlib_reverse_bytes(single, 1);
    ASSERT(s == EXLIB_OK, "reverse n=1 = OK");
    ASSERT(single[0] == 42, "single byte tidak berubah");
}

static void test_format_size(void) {
    char buf[32];
    exlib_status s = exlib_format_size(0, buf, sizeof(buf));
    ASSERT(s == EXLIB_OK, "format 0 = OK");
    ASSERT(strcmp(buf, "0.00 B") == 0, "0 bytes = '0.00 B'");

    s = exlib_format_size(1024ULL, buf, sizeof(buf));
    ASSERT(s == EXLIB_OK, "format 1024 = OK");
    ASSERT(strcmp(buf, "1.00 KB") == 0, "1024 = '1.00 KB'");

    s = exlib_format_size(1572864ULL, buf, sizeof(buf));
    ASSERT(s == EXLIB_OK, "format 1.5MB = OK");
    ASSERT(strcmp(buf, "1.50 MB") == 0, "1.5MB = '1.50 MB'");

    s = exlib_format_size(100, buf, 5);
    ASSERT(s == EXLIB_ERR_RANGE, "buffer kecil = ERR_RANGE");
}

static void test_contains(void) {
    exlib_status s = exlib_contains("hello world", "world");
    ASSERT(s == EXLIB_OK, "substring ditemukan");

    s = exlib_contains("hello world", "xyz");
    ASSERT(s == EXLIB_ERR_NOT_FOUND, "substring tidak ditemukan");

    s = exlib_contains("abc", "");
    ASSERT(s == EXLIB_OK, "needle kosong = OK");

    s = exlib_contains(NULL, "x");
    ASSERT(s == EXLIB_ERR_NULL, "NULL haystack = ERR_NULL");
}

int main(void) {
    test_version();
    test_mean();
    test_stddev();
    test_reverse_bytes();
    test_format_size();
    test_contains();

    printf("\n=== Hasil Test examplelib ===\n");
    printf("Total : %d\n", tests_run);
    printf("Lulus : %d\n", tests_pass);
    printf("Gagal : %d\n", tests_run - tests_pass);
    return (tests_run == tests_pass) ? 0 : 1;
}

Jalankan test (setelah bangun library):

bash
gcc -std=c11 -Wall -Wextra -fPIC -Iinclude -c src/examplelib.c -o src/examplelib.o
ar rcs libexamplelib.a src/examplelib.o
gcc -std=c11 -Wall -Wextra -Iinclude -o test_examplelib \
    tests/test_examplelib.c -L. -lexamplelib -lm
./test_examplelib

Output yang Dihasilkan:

text

=== Hasil Test examplelib ===
Total : 28
Lulus : 28
Gagal : 0

Mari kita telaah baris-baris kode di atas:

  1. Baris 8-15 (ASSERT macro): Sama dengan proyek 7 dan 8. Pattern do { ... } while (0) memungkinkan macro dipakai seperti statement. __LINE__ dicatat untuk debugging.
  2. Baris 17-23 (test_version): Smoke test paling dasar. Verifikasi string versi dan strerror mengembalikan pesan yang benar. Bila versi berubah (mis. ke 1.1.0), test ini perlu diupdate.
  3. Baris 25-37 (test_mean): Test happy path dengan 5 data, lalu dua kasus error (NULL dan n=0). Floating point comparison memakai fabs dengan epsilon 1e-9 untuk menghindari masalah presisi double.
  4. Baris 39-46 (test_stddev): Data 2,4,4,4,5,5,7,9 dipilih karena standar deviasi populasinya tepat 2.0 (variance = 4.0). Memakai dataset dengan hasil yang “bulat” memudahkan verifikasi tanpa kalkulator.
  5. Baris 48-60 (test_reverse_bytes): Test reverse pada array 5 elemen, lalu edge case: NULL input dan n=1. Bila n=1, tidak ada swap yang terjadi (loop while i < j tidak dieksekusi).
  6. Baris 62-77 (test_format_size): Test 4 kasus: 0 bytes, 1024 (1 KB), 1572864 (1.5 MB), dan buffer terlalu kecil. Verifikasi tidak hanya kode return, tapi juga string output yang dihasilkan.
  7. Baris 79-90 (test_contains): Test substring ditemukan, tidak ditemukan, needle kosong (selalu match), dan NULL haystack. Empat kasus mencakup semua branch logika.
  8. Baris 92-104 (main): Panggil semua test function, lalu cetak ringkasan. Exit code 0 bila semua lulus, 1 bila ada yang gagal. Penting untuk CI/CD.
  9. Build dengan library (-L. -lexamplelib): Test di-link ke static library yang sudah dibangun. Pola ini memastikan test memakai library seperti yang akan dipakai pengguna nyata, bukan compile ulang dari source.

Kode Lengkap #

Kode lengkap untuk test_examplelib.c ada di direktori tests/ proyek. Total 28 assertion yang semuanya lulus.

Pengujian #

Verifikasi test memang menangkap bug dengan sengaja memodifikasi library:

bash
# Skenario: hapus baris "if (data == NULL)" di exlib_mean
# Kompilasi ulang library + test
make clean && make test
# Test test_mean "mean(NULL) = ERR_NULL" harus FAIL
# Kembalikan kode, build ulang, test harus lulus lagi

Pengujian tambahan:

  • Property-based test: generate random array dan verifikasi exlib_mean selalu antara min dan max.
  • Stress test: array dengan 1 juta elemen, pastikan tidak ada overflow.
  • Memory test dengan valgrind: pastikan tidak ada read/write out-of-bounds.
bash
valgrind --leak-check=full --error-exitcode=1 ./test_examplelib