Semua modul sudah terpisah dan berfungsi. Tetapi tanpa rangkaian test otomatis, kita tidak yakin perubahan di masa depan tidak akan merusak fitur yang sudah jalan. Pada bagian terakhir ini, kita bangun unit test runner tanpa framework eksternal dan susun Makefile yang mengotomatisasi build + test + clean.

Konsep #

Unit test adalah test yang memeriksa satu unit kode (biasanya satu fungsi) secara terisolasi. Untuk C, kita tidak harus memakai framework seperti Unity atau Check — kita bisa menulis runner sendiri dengan dua macro sederhana:

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

Macro ini menghitung total test, jumlah yang lulus, dan mencetak pesan kegagalan dengan nomor baris. Pattern do { ... } while (0) memungkinkan macro dipakai seperti statement biasa, lengkap dengan semicolon.

Makefile mengotomatisasi langkah build yang berulang. Pattern dasar Makefile:

makefile
target: prerequisites
	recipe

target adalah nama yang bisa dipanggil dengan make target. prerequisites adalah file atau target lain yang harus ada sebelum recipe dijalankan. recipe adalah perintah shell (wajib diawali TAB, bukan spasi).

Implementasi #

Test runner tests.c yang lengkap:

c
/* tests.c - Unit test tanpa framework eksternal */
#include <stdio.h>
#include <string.h>
#include "inventory.h"
#include "storage.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_product_create_valid(void) {
    Product p;
    int rc = product_create(&p, 1, "Beras 5kg", 65000.0, 20);
    ASSERT(rc == 0, "product_create valid harus sukses");
    ASSERT(p.id == 1, "id harus 1");
    ASSERT(strcmp(p.name, "Beras 5kg") == 0, "nama harus sesuai");
    ASSERT(p.price == 65000.0, "harga harus sesuai");
    ASSERT(p.stock == 20, "stok harus 20");
    ASSERT(product_is_valid(&p), "produk harus valid");
}

static void test_inventory_add_and_find(void) {
    Inventory inv;
    inventory_init(&inv);
    int id1 = inventory_add(&inv, "Beras", 65000.0, 10);
    int id2 = inventory_add(&inv, "Gula", 15000.0, 5);
    ASSERT(id1 == 1, "id produk pertama harus 1");
    ASSERT(id2 == 2, "id produk kedua harus 2");
    ASSERT(inv.count == 2, "jumlah harus 2");

    Product *p = inventory_find_by_id(&inv, 2);
    ASSERT(p != NULL, "id=2 harus ditemukan");
    ASSERT(strcmp(p->name, "Gula") == 0, "nama produk id=2 harus Gula");
}

static void test_storage_roundtrip(void) {
    Inventory inv;
    inventory_init(&inv);
    inventory_add(&inv, "Beras", 65000.0, 10);
    inventory_add(&inv, "Gula", 15000.0, 5);

    const char *path = "/tmp/test_inventory.csv";
    int written = storage_save(&inv, path);
    ASSERT(written == 2, "harus menulis 2 baris");

    Inventory loaded;
    int read = storage_load(&loaded, path);
    ASSERT(read == 2, "harus membaca 2 baris");
    ASSERT(loaded.count == 2, "count harus 2 setelah load");
    ASSERT(loaded.next_id == 3, "next_id harus 3 setelah load");
}

int main(void) {
    test_product_create_valid();
    test_inventory_add_and_find();
    test_storage_roundtrip();

    printf("\n=== Hasil Test ===\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;
}

Makefile lengkap untuk build dan test:

makefile
# Makefile untuk Pengelola Inventaris
CC      = gcc
CFLAGS  = -std=c11 -Wall -Wextra -pedantic
LDFLAGS = -lm

SRC_MAIN = main.c product.c inventory.c storage.c
SRC_TEST = tests.c product.c inventory.c storage.c

BIN_MAIN = inventory
BIN_TEST = test_inventory

.PHONY: all test clean

all: $(BIN_MAIN)

$(BIN_MAIN): $(SRC_MAIN)
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

$(BIN_TEST): $(SRC_TEST)
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

test: $(BIN_TEST)
	./$(BIN_TEST)

clean:
	rm -f $(BIN_MAIN) $(BIN_TEST) *.o inventory.csv

Jalankan build dan test:

bash
make test

Output yang Dihasilkan:

text
gcc -std=c11 -Wall -Wextra -pedantic -o test_inventory tests.c product.c inventory.c storage.c -lm
./test_inventory

=== Hasil Test ===
Total : 35
Lulus : 35
Gagal : 0

Mari kita telaah baris-baris kode di atas:

  1. Baris 8-16 (ASSERT macro): Pattern do { ... } while (0) memungkinkan macro dipakai seperti statement biasa. Tanpa pola ini, macro yang berisi blok { } bisa bermasalah bila dipakai di if/else tanpa kurung kurawal.
  2. Baris 25-31 (test_product_create_valid): Test pertama memeriksa happy path. Lima ASSERT berurutan memverifikasi setiap field Product terisi dengan benar.
  3. Baris 33-43 (test_inventory_add_and_find): Test add + find. Pola “tambah beberapa item lalu cari salah satunya” adalah test klasik untuk operasi CRUD.
  4. Baris 47-60 (test_storage_roundtrip): Test roundtrip save-load. Pola: simpan data → load ke struct baru → bandingkan. Bila field next_id tidak konsisten setelah load, test akan menangkap.
  5. Baris 64-65 (exit code): return (tests_run == tests_pass) ? 0 : 1 memberi exit code 0 bila semua lulus, 1 bila ada yang gagal. Penting untuk CI/CD: skrip bisa coba ./test_inventory && echo OK || echo FAIL.
  6. Baris 17-20 Makefile (SRC_MAIN, SRC_TEST): Variabel daftar file sumber. Build utama dan test berbagi file modul (product.c, inventory.c, storage.c) tetapi berbeda entry point (main.c vs tests.c).
  7. Baris 26 Makefile (.PHONY: all test clean): Deklarasi target non-file. make test selalu jalankan recipe meskipun sudah ada file bernama test.
  8. Baris 31-32 Makefile (test: $(BIN_TEST)): Target test bergantung pada binary test. Make akan otomatis build bila binary belum ada atau sumber berubah.

Kode Lengkap #

Kode lengkap untuk tests.c dan Makefile ada di direktori proyek. tests.c berisi 7 fungsi test yang mencakup validasi model, CRUD inventory, pencarian, sort, dan storage roundtrip. Total 35 assertion yang semuanya lulus.

Pengujian #

Selain test otomatis, lakukan juga smoke test manual pada aplikasi akhir:

bash
make                            # Bangun aplikasi utama
./inventory                     # Jalankan aplikasi
# Pilih 1 (Tambah produk), isi data: "Beras 5kg", 65000, 10
# Pilih 1 lagi: "Gula Pasir", 15000, 5
# Pilih 9 (Simpan ke file)
# Pilih Q (Keluar)
# Jalankan lagi:
./inventory
# Aplikasi akan otomatis memuat 2 produk dari inventory.csv

Pengujian tambahan yang dianjurkan:

  • Test dengan file CSV rusak (edit manual, hapus kolom) → program harus tetap jalan, melewatkan baris rusak.
  • Test dengan file CSV berisi id tidak berurutan (mis. 1, 3, 5) → pastikan next_id di-set ke 6.
  • Test dengan inventory penuh (256 produk) → pastikan inventory_add mengembalikan -1.
bash
make clean    # Bersihkan artefact build