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:

  1. Validasi model — transaction_create dengan berbagai input invalid.
  2. Operasi koleksi — add/delete dengan id berurutan, summary yang benar.
  3. Filter — filter by category case-insensitive dan by date range.
  4. Persistensi — roundtrip save-load dengan next_id benar.
  5. Laporan — report_by_category menghasilkan 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):

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

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:

bash
make test

Output yang Dihasilkan:

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

Mari kita telaah baris-baris kode di atas:

  1. Baris 27-31 (test_transaction_create): Test happy path dengan dua transaksi (income dan expense). Memverifikasi field terisi dengan benar dan signed_value benar untuk masing-masing tipe.
  2. 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.
  3. 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) karena double presisi cukup untuk angka sebesar ini.
  4. 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.
  5. Baris 66-74 (test_finance_filter_date): 4 transaksi dengan tanggal berbeda. Filter rentang 02-05 harus mengembalikan 2 transaksi (tanggal 02 dan 05). Filter from >= 06 harus 1, to <= 02 harus 2.
  6. Baris 77-89 (test_storage_roundtrip): Save 2 transaksi, load kembali, verifikasi count dan next_id. Test ini paling penting untuk memastikan tidak ada field yang hilang saat persistensi.
  7. 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.
  8. 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.c vs tests.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:

bash
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.csv

Pengujian tambahan:

  • Test dengan transaksi jumlah besar (1 miliar) → pastikan double tidak 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.
bash
make clean