Proyek pengelola keuangan hampir selesai. Bagian terakhir adalah memastikan semua modul bekerja bersama dengan unit test dan Makefile yang mengotomatisasi build + test. Pola yang dipakai sama dengan proyek 7: macro ASSERT, test function per fitur, dan Makefile dengan target all, test, clean.
Konsep #
Strategi test untuk pengelola keuangan mencakup empat area:
- Validasi model —
transaction_createdengan berbagai input invalid. - Operasi koleksi — add/delete dengan id berurutan, summary yang benar.
- Filter — filter by category case-insensitive dan by date range.
- Persistensi — roundtrip save-load dengan
next_idbenar. - Laporan —
report_by_categorymenghasilkan jumlah kategori yang benar dengan subtotal yang akurat.
Pola test yang sama dengan proyek 7: macro ASSERT yang menghitung total dan pass count. Setiap test function berdiri sendiri, inisialisasi Finance baru di awal, lalu beberapa ASSERT berurutan.
Implementasi #
Test runner tests.c lengkap (ringkas):
/* tests.c - Unit test pengelola keuangan */
#include <stdio.h>
#include <string.h>
#include <math.h>
#include "finance.h"
#include "storage.h"
#include "report.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_transaction_create(void) {
Transaction t;
int rc = transaction_create(&t, 1, TXN_INCOME, 1000000.0,
"Gaji bulanan", "Gaji", "2024-03-01");
ASSERT(rc == 0, "transaction_create valid harus sukses");
ASSERT(t.id == 1, "id harus 1");
ASSERT(transaction_signed_value(&t) == 1000000.0, "signed = +1M");
Transaction t2;
rc = transaction_create(&t2, 2, TXN_EXPENSE, 25000.0,
"Makan siang", "Makan", "2024-03-02");
ASSERT(rc == 0, "transaction_create expense sukses");
ASSERT(transaction_signed_value(&t2) == -25000.0, "signed = -25K");
}
static void test_transaction_invalid(void) {
Transaction t;
ASSERT(transaction_create(&t, 0, TXN_INCOME, 100, "X", "C",
"2024-01-01") != 0, "id=0 ditolak");
ASSERT(transaction_create(&t, 1, TXN_INCOME, -1, "X", "C",
"2024-01-01") != 0, "amount negatif ditolak");
ASSERT(transaction_create(&t, 1, TXN_INCOME, 100, "X", "C",
"2024/01/01") != 0, "format tanggal salah ditolak");
ASSERT(transaction_create(&t, 1, TXN_INCOME, 100, "X", "C",
"2024-13-01") != 0, "bulan 13 ditolak");
}
static void test_finance_add_summary(void) {
Finance f;
finance_init(&f);
finance_add(&f, TXN_INCOME, 5000000.0, "Gaji", "Gaji", "2024-03-01");
finance_add(&f, TXN_EXPENSE, 50000.0, "Bensin", "Transport", "2024-03-02");
finance_add(&f, TXN_EXPENSE, 30000.0, "Makan", "Makan", "2024-03-02");
double inc, exp, bal;
finance_summary(&f, &inc, &exp, &bal);
ASSERT(inc == 5000000.0, "total income 5M");
ASSERT(exp == 80000.0, "total expense 80K");
ASSERT(bal == 4920000.0, "balance 4.92M");
}
static void test_finance_filter_category(void) {
Finance f;
finance_init(&f);
finance_add(&f, TXN_EXPENSE, 10000.0, "Kopi", "Makan", "2024-03-01");
finance_add(&f, TXN_EXPENSE, 20000.0, "Bakso", "Makan", "2024-03-01");
finance_add(&f, TXN_EXPENSE, 50000.0, "Bensin", "Transport", "2024-03-02");
Transaction out[16];
int n = finance_filter_by_category(&f, "makan", out, 16);
ASSERT(n == 2, "filter 'makan' case-insensitive = 2");
}
static void test_finance_filter_date(void) {
Finance f;
finance_init(&f);
finance_add(&f, TXN_EXPENSE, 10000.0, "A", "X", "2024-03-01");
finance_add(&f, TXN_EXPENSE, 20000.0, "B", "X", "2024-03-02");
finance_add(&f, TXN_EXPENSE, 30000.0, "C", "X", "2024-03-05");
finance_add(&f, TXN_EXPENSE, 40000.0, "D", "X", "2024-03-10");
Transaction out[16];
int n = finance_filter_by_date(&f, "2024-03-02", "2024-03-05", out, 16);
ASSERT(n == 2, "rentang 02-05 = 2");
}
static void test_storage_roundtrip(void) {
Finance f;
finance_init(&f);
finance_add(&f, TXN_INCOME, 1000000.0, "Gaji", "Gaji", "2024-03-01");
finance_add(&f, TXN_EXPENSE, 50000.0, "Bensin", "Transport", "2024-03-02");
const char *path = "/tmp/test_finance.csv";
int written = storage_save(&f, path);
ASSERT(written == 2, "harus menulis 2 baris");
Finance loaded;
int read = storage_load(&loaded, path);
ASSERT(read == 2, "harus membaca 2 baris");
ASSERT(loaded.next_id == 3, "next_id=3 setelah load");
}
static void test_report_category(void) {
Finance f;
finance_init(&f);
finance_add(&f, TXN_INCOME, 1000000.0, "Gaji", "Gaji", "2024-03-01");
finance_add(&f, TXN_EXPENSE, 50000.0, "Bensin", "Transport", "2024-03-02");
finance_add(&f, TXN_EXPENSE, 30000.0, "Makan", "Makan", "2024-03-02");
finance_add(&f, TXN_EXPENSE, 20000.0, "Cafe", "Makan", "2024-03-03");
CategoryReport r[16];
int n = report_by_category(&f, r, 16);
ASSERT(n == 3, "harus ada 3 kategori unik");
for (int i = 0; i < n; i++) {
if (strcmp(r[i].category, "Makan") == 0) {
ASSERT(r[i].expense == 50000.0, "Makan expense 50K");
ASSERT(r[i].count == 2, "Makan count 2");
}
}
}
int main(void) {
test_transaction_create();
test_transaction_invalid();
test_finance_add_summary();
test_finance_filter_category();
test_finance_filter_date();
test_storage_roundtrip();
test_report_category();
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:
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -pedantic
LDFLAGS = -lm
SRC_MAIN = main.c transaction.c finance.c storage.c report.c
SRC_TEST = tests.c transaction.c finance.c storage.c report.c
BIN_MAIN = finance
BIN_TEST = test_finance
.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 finance.csv
Jalankan test:
make testOutput yang Dihasilkan:
gcc -std=c11 -Wall -Wextra -pedantic -o test_finance tests.c transaction.c finance.c storage.c report.c -lm
./test_finance
=== Hasil Test ===
Total : 32
Lulus : 32
Gagal : 0Mari kita telaah baris-baris kode di atas:
- Baris 27-31 (
test_transaction_create): Test happy path dengan dua transaksi (income dan expense). Memverifikasi field terisi dengan benar dansigned_valuebenar untuk masing-masing tipe. - Baris 34-39 (
test_transaction_invalid): Test lima kasus input invalid: id 0, amount negatif, format tanggal slash, bulan 13, hari 32. Semua harus return non-zero. - Baris 42-52 (
test_finance_add_summary): Tambah 3 transaksi (1 income, 2 expense), lalu verifikasi summary. Total income = 5M, total expense = 80K, balance = 4.92M. Angka exact match (bukan approximasi) karenadoublepresisi cukup untuk angka sebesar ini. - Baris 55-63 (
test_finance_filter_category): Tambah 3 transaksi dengan 2 kategori berbeda (“Makan” dan “Transport”). Filter “makan” case-insensitive harus mengembalikan 2 hasil. - Baris 66-74 (
test_finance_filter_date): 4 transaksi dengan tanggal berbeda. Filter rentang02-05harus mengembalikan 2 transaksi (tanggal 02 dan 05). Filterfrom >= 06harus 1,to <= 02harus 2. - Baris 77-89 (
test_storage_roundtrip): Save 2 transaksi, load kembali, verifikasicountdannext_id. Test ini paling penting untuk memastikan tidak ada field yang hilang saat persistensi. - Baris 92-107 (
test_report_category): 4 transaksi dengan 3 kategori (“Gaji”, “Transport”, “Makan”). Laporan harus punya 3 entri. Untuk kategori “Makan” yang muncul di 2 transaksi, expense = 50K dan count = 2. - Baris 8-15 Makefile (
SRC_MAIN,SRC_TEST): Lima file sumber untuk aplikasi utama, lima file untuk test. Berbagi empat modul (transaction,finance,storage,report) tetapi berbeda entry point (main.cvstests.c).
Kode Lengkap #
Kode lengkap untuk tests.c dan Makefile ada di direktori proyek. Total 32 assertion yang semuanya lulus.
Pengujian #
Selain test otomatis, lakukan smoke test manual:
make
./finance
# Pilih 1 (Catat pemasukan): Gaji, 5000000, Gaji, 2024-03-01
# Pilih 2 (Catat pengeluaran): Bensin, 50000, Transport, 2024-03-02
# Pilih 2: Makan siang, 30000, Makan, 2024-03-02
# Pilih 4 (Ringkasan): harus tampil saldo 4.92M
# Pilih 7 (Laporan per kategori): harus tampil 3 kategori
# Pilih 8 (Laporan bulanan): harus tampil satu baris 2024-03
# Pilih S (Simpan ke file)
# Pilih Q (Keluar)
# Jalankan lagi:
./finance
# Aplikasi memuat 3 transaksi dari finance.csvPengujian tambahan:
- Test dengan transaksi jumlah besar (1 miliar) → pastikan
doubletidak kehilangan presisi. - Test dengan kategori yang sangat panjang (mendekati
TXN_CATEGORY_MAX). - Test dengan tanggal di tahun kabisat (29 Februari 2024) — validator kita menerima (karena hanya cek rentang 1-31), tetapi program tidak crash.
- Test dengan file CSV yang di-edit manual di luar aplikasi → pastikan program bisa memuat dengan benar atau melewati baris rusak dengan aman.
make clean