Menjalankan test secara manual dengan mengetik gcc ... && ./test untuk setiap file test cepat menjadi tidak scalabel—sebuah proyek dengan 50 file test membutuhkan 50 perintah. Test automation menyelesaikan masalah ini: sebuah perintah tunggal seperti make test meng-compile seluruh test, menjalankannya, mengumpulkan hasil, dan mengembalikan kode exit yang menandakan keberhasilan atau kegagalan. Pada chapter ini kita membangun Makefile dengan target make test yang menjalankan tiga file test secara otomatis, lalu mendemonstrasikan bagaimana kegagalan satu test menghentikan pipeline dengan kode exit non-zero—sinyal yang dibutuhkan CI untuk menandai build gagal.

Struktur Proyek Test #

Proyek terdiri dari modul produksi (math_ops) dan dua file test. Struktur file:

text
math_ops.h             # deklarasi publik
math_ops.c             # implementasi
test_math_ops.c        # test 1
test_math_ops_extra.c  # test 2
Makefile               # otomasi

Kode produksi dan test berikut sangat sederhana agar fokus tetap pada struktur Makefile:

c
/* File: math_ops.h */
#ifndef MATH_OPS_H
#define MATH_OPS_H
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
#endif
c
/* File: math_ops.c */
#include "math_ops.h"
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
c
/* File: test_math_ops.c */
#include <assert.h>
#include <stdio.h>
#include "math_ops.h"

int main(void) {
    assert(add(2, 3) == 5);
    assert(sub(10, 4) == 6);
    assert(mul(7, 6) == 42);
    printf("test_math_ops: 3 assertions lulus\n");
    return 0;
}
c
/* File: test_math_ops_extra.c */
#include <assert.h>
#include <stdio.h>
#include "math_ops.h"

int main(void) {
    assert(add(-5, 5) == 0);
    assert(sub(0, 0) == 0);
    assert(mul(0, 999) == 0);
    printf("test_math_ops_extra: 3 assertions lulus\n");
    return 0;
}

Makefile dengan Target make test #

Makefile berikut mendefinisikan tiga target utama: all (build semua test), test (build + jalankan semua test dengan pelaporan terstruktur), dan clean (hapus binary). Variabel TESTS mendaftar nama-nama executable test, sehingga menambah test baru hanya memerlukan satu baris.

makefile
# File: Makefile
CC      = gcc
CFLAGS  = -std=c11 -Wall -Wextra
SRCS    = math_ops.c
TESTS   = test_math_ops test_math_ops_extra

.PHONY: all test clean

all: $(TESTS)

test: $(TESTS)
	@echo "=== Menjalankan test suite ==="
	@failed=0; \
	for t in $(TESTS); do \
	    echo "[RUN ] $$t"; \
	    ./$$t; \
	    rc=$$?; \
	    if [ $$rc -eq 0 ]; then \
		echo "[PASS] $$t"; \
	    else \
		echo "[FAIL] $$t (exit $$rc)"; \
		failed=$$((failed + 1)); \
	    fi; \
	done; \
	echo "=== Hasil: $$(( $(words $(TESTS)) - $$failed )) / $(words $(TESTS)) lulus ==="; \
	if [ $$failed -gt 0 ]; then exit 1; fi

test_math_ops: test_math_ops.c $(SRCS) math_ops.h
	$(CC) $(CFLAGS) -o $@ test_math_ops.c $(SRCS)

test_math_ops_extra: test_math_ops_extra.c $(SRCS) math_ops.h
	$(CC) $(CFLAGS) -o $@ test_math_ops_extra.c $(SRCS)

clean:
	rm -f $(TESTS) *.o

Jalankan make test:

bash
make test

Output yang Dihasilkan:

text
gcc -std=c11 -Wall -Wextra -o test_math_ops test_math_ops.c math_ops.c
gcc -std=c11 -Wall -Wextra -o test_math_ops_extra test_math_ops_extra.c math_ops.c
=== Menjalankan test suite ===
[RUN ] test_math_ops
test_math_ops: 3 assertions lulus
[PASS] test_math_ops
[RUN ] test_math_ops_extra
test_math_ops_extra: 3 assertions lulus
[PASS] test_math_ops_extra
=== Hasil: 2 / 2 lulus ===

Mari kita telaah baris-baris kunci Makefile di atas:

  1. Baris 6 (TESTS = test_math_ops test_math_ops_extra): Daftar executable test. make memperlakukan daftar ini sebagai prerequisites target all dan test. Untuk menambah test baru, cukup tambahkan nama executable ke daftar ini dan definisikan rule build di bawah.
  2. Baris 8 (.PHONY: all test clean): Deklarasi target phony—target yang bukan file. Tanpa .PHONY, bila ada file bernama test di direktori, make test mungkin tidak dijalankan ulang karena make menganggap target sudah up-to-date.
  3. Baris 12 (test: $(TESTS)): Target test bergantung pada semua executable di $(TESTS). Make pertama-tama membangun setiap executable (lewat rule eksplisit di bawah), baru kemudian menjalankan recipe shell.
  4. Baris 14-25 (recipe shell loop): Loop for t in $(TESTS) menjalankan setiap test. Variabel $$t adalah referensi shell (dengan $$ agar make tidak meng-expand). $$? menangkap kode exit setiap test. Bila kode exit bukan 0, test ditandai FAIL dan penghitung failed bertambah.
  5. Baris 24 (if [ $$failed -gt 0 ]; then exit 1; fi): Bila ada kegagalan, recipe exit 1. Kode exit ini menjadi kode exit target make test—sinyal untuk CI bahwa build gagal.
  6. Baris 27-28 (test_math_ops: test_math_ops.c $(SRCS) math_ops.h): Rule build eksplisit. Prerequisites mencakup file test, file produksi ($(SRCS)), dan header. Bila salah satu berubah, make akan rebuild test runner.

Pelaporan Kegagalan #

Saat satu test gagal, Makefile tetap menjalankan test berikutnya (continue-on-failure) sehingga kita melihat semua kegagalan sekaligus. Pada akhir, make test mengembalikan kode exit non-zero sehingga CI pipeline berhenti. Berikut demonstrasi dengan menambah test yang sengaja salah:

c
/* File: test_broken.c — test yang sengaja gagal */
#include <assert.h>
#include <stdio.h>
#include "math_ops.h"

int main(void) {
    /* Bug: add(2, 3) bukan 6 */
    assert(add(2, 3) == 6);
    printf("test_broken: lulus\n");
    return 0;
}

Tambahkan test_broken ke TESTS di Makefile, lalu jalankan ulang make test. Output yang Dihasilkan (dipotong):

text
gcc -std=c11 -Wall -Wextra -o test_math_ops test_math_ops.c math_ops.c
gcc -std=c11 -Wall -Wextra -o test_math_ops_extra test_math_ops_extra.c math_ops.c
gcc -std=c11 -Wall -Wextra -o test_broken test_broken.c math_ops.c
=== Menjalankan test suite ===
[RUN ] test_math_ops
test_math_ops: 3 assertions lulus
[PASS] test_math_ops
[RUN ] test_math_ops_extra
test_math_ops_extra: 3 assertions lulus
[PASS] test_math_ops_extra
[RUN ] test_broken
test_broken: test_broken.c:7: main: Assertion `add(2, 3) == 6' failed.
Aborted
[FAIL] test_broken (exit 134)
=== Hasil: 2 / 3 lulus ===
make: *** [Makefile:12: test] Error 1

Meskipun test_broken gagal, dua test lain tetap dijalankan dan lulus. Ringkasan 2 / 3 lulus langsung terlihat. Baris terakhir make: *** [Makefile:12: test] Error 1 menandakan bahwa make berhenti karena kode exit 1 dari recipe—sinyal yang dibutuhkan CI.

Integrasi dengan Continuous Integration #

CI seperti GitHub Actions, GitLab CI, atau Jenkins memanggil make test sebagai salah satu langkah pipeline. Konfigurasi tipikal (dalam GitHub Actions YAML) terlihat seperti:

yaml
name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build & test
        run: make test

CI menjalankan make test dan memeriksa kode exit. Kode 0 berarti semua test lulus—pipeline hijau. Kode non-zero berarti ada kegagalan—pipeline merah dan tim diberi notifikasi.

Untuk pipeline yang lebih kaya, kita dapat menambahkan target terpisah:

  • make test-unit: Hanya unit test (cepat, dijalankan setiap commit).
  • make test-integration: Integration test (lebih lambat, dijalankan sebelum merge).
  • make test-coverage: Menjalankan test dengan flag coverage dan mengumpulkan laporan gcov.
  • make test-asan: Menjalankan test dengan AddressSanitizer + UBSan.