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:
#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:
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:
/* 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 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:
make testOutput yang Dihasilkan:
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 : 0Mari kita telaah baris-baris kode di atas:
- Baris 8-16 (
ASSERTmacro): Patterndo { ... } while (0)memungkinkan macro dipakai seperti statement biasa. Tanpa pola ini, macro yang berisi blok{ }bisa bermasalah bila dipakai diif/elsetanpa kurung kurawal. - Baris 25-31 (
test_product_create_valid): Test pertama memeriksa happy path. Lima ASSERT berurutan memverifikasi setiap fieldProductterisi dengan benar. - 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. - Baris 47-60 (
test_storage_roundtrip): Test roundtrip save-load. Pola: simpan data → load ke struct baru → bandingkan. Bila fieldnext_idtidak konsisten setelah load, test akan menangkap. - Baris 64-65 (exit code):
return (tests_run == tests_pass) ? 0 : 1memberi exit code 0 bila semua lulus, 1 bila ada yang gagal. Penting untuk CI/CD: skrip bisa coba./test_inventory && echo OK || echo FAIL. - 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.cvstests.c). - Baris 26 Makefile (
.PHONY: all test clean): Deklarasi target non-file.make testselalu jalankan recipe meskipun sudah ada file bernamatest. - Baris 31-32 Makefile (
test: $(BIN_TEST)): Targettestbergantung 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:
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.csvPengujian 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_iddi-set ke 6. - Test dengan inventory penuh (256 produk) → pastikan
inventory_addmengembalikan -1.
make clean # Bersihkan artefact build