Setiap modul mungkin lulus unit test-nya sendiri, tetapi pemasangan antar modul sering menyimpan bug tak terduga: kontrak yang berbeda antara parser dan calculator, edge case yang muncul hanya ketika data mengalir dari satu modul ke modul lain, atau state yang berubah saat pipeline dieksekusi. Integration test memverifikasi bahwa modul-modul bekerja sama sesuai harapan. Pada chapter ini kita membangun dua modul (parser dan calc) lalu menguji keseluruhan alur dari string input sampai hasil perhitungan.
Memisahkan Modul agar Testable #
Strategi pertama dalam integration test adalah memastikan setiap modul memiliki kontrak yang jelas melalui header. Modul parser mengubah string "<int> <op> <int>" menjadi tiga nilai (a, op, b). Modul calc menerapkan operasi op pada a dan b. Integration test menguji apakah output parser dapat langsung dikonsumsi calc tanpa konversi tambahan.
/* File: parser.h */
#ifndef PARSER_H
#define PARSER_H
#include <stddef.h>
/* Hasil parse: 0 = sukses, -1 = error */
int parse_expression(const char *s, int *out_a, int *out_b, char *out_op);
#endif/* File: parser.c */
#include "parser.h"
#include <ctype.h>
#include <stdio.h>
/* Parser sangat sederhana: "<int> <op> <int>" */
int parse_expression(const char *s, int *out_a, int *out_b, char *out_op) {
if (s == NULL || out_a == NULL || out_b == NULL || out_op == NULL) {
return -1;
}
int a, b;
char op;
int matched = sscanf(s, "%d %c %d", &a, &op, &b);
if (matched != 3) {
return -1;
}
/* Validasi operator yang dikenal */
if (op != '+' && op != '-' && op != '*' && op != '/') {
return -1;
}
*out_a = a;
*out_b = b;
*out_op = op;
return 0;
}/* File: calc.h */
#ifndef CALC_H
#define CALC_H
/* Kembalikan hasil; bila divide by zero, kembalikan 0 dan set *err=1 */
int calc_apply(int a, char op, int b, int *err);
#endif/* File: calc.c */
#include "calc.h"
#include <stddef.h>
int calc_apply(int a, char op, int b, int *err) {
if (err == NULL) return 0;
*err = 0;
switch (op) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/':
if (b == 0) { *err = 1; return 0; }
return a / b;
default:
*err = 1;
return 0;
}
}Test yang Menyatukan Parser dan Calculator #
Integration test berikut memverifikasi seluruh alur: dari string input, melalui parser, ke calculator, sampai menghasilkan angka atau sinyal error. Test ini tidak lagi menguji parse_expression atau calc_apply secara terpisah—melainkan kontrak yang menghubungkan keduanya.
/* File: test_integration.c — menguji parser + calc bekerja sama */
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "parser.h"
#include "calc.h"
static int tests_run = 0;
static void test_parse_and_calc_add(void) {
tests_run++;
int a, b, err;
char op;
assert(parse_expression("10 + 5", &a, &b, &op) == 0);
assert(a == 10 && b == 5 && op == '+');
int r = calc_apply(a, op, b, &err);
assert(err == 0 && r == 15);
printf("[OK] '10 + 5' = %d\n", r);
}
static void test_parse_and_calc_div_zero(void) {
tests_run++;
int a, b, err;
char op;
assert(parse_expression("20 / 0", &a, &b, &op) == 0);
int r = calc_apply(a, op, b, &err);
assert(err == 1);
printf("[OK] '20 / 0' menetapkan err=1 (r=%d)\n", r);
}
static void test_parse_invalid_op(void) {
tests_run++;
int a, b;
char op;
assert(parse_expression("7 % 3", &a, &b, &op) == -1);
printf("[OK] operator '%%' ditolak parser\n");
}
static void test_parse_garbage(void) {
tests_run++;
int a, b;
char op;
assert(parse_expression("halo dunia", &a, &b, &op) == -1);
printf("[OK] teks non-numerik ditolak parser\n");
}
int main(void) {
test_parse_and_calc_add();
test_parse_and_calc_div_zero();
test_parse_invalid_op();
test_parse_garbage();
printf("\nIntegration test: %d lulus.\n", tests_run);
return 0;
}Build dan jalankan:
gcc -std=c11 -Wall -Wextra -o test_integration \
test_integration.c parser.c calc.c && ./test_integrationOutput yang Dihasilkan:
[OK] '10 + 5' = 15
[OK] '20 / 0' menetapkan err=1 (r=0)
[OK] operator '%' ditolak parser
[OK] teks non-numerik ditolak parser
Integration test: 4 lulus.Mari kita telaah baris-baris kode di atas:
- Baris 12-13 (
parse_expression("10 + 5", ...)): Test pertama memverifikasi happy path. Parser mengembalikan0(sukses), dan calculator menghasilkan15tanpa error. Test ini menyatukan dua modul dalam satu asersi. - Baris 25 (
assert(parse_expression("20 / 0", ...) == 0);): Parser menerima"20 / 0"sebagai ekspresi valid (operator/dikenal, kedua operand numerik). Tanggung jawab parser hanya soal bentuk, bukan semantik. - Baris 27 (
int r = calc_apply(a, op, b, &err);): Calculator menemukan pembagian dengan nol dan menetapkanerr = 1. Integration test memverifikasi bahwa error code1di-propagate dengan benar ke pemanggil—tidak ada crash. - Baris 34 (
assert(parse_expression("7 % 3", ...) == -1);): Test memastikan parser menolak operator%(modulo) yang tidak didukung calculator. Tanpa test ini, parser mungkin menerima%lalu calculator mengembalikan error generik—bug menjadi sulit dilacak. - Baris 42 (
assert(parse_expression("halo dunia", ...) == -1);): Test memastikansscanfgagal mengonversi teks non-numerik, dan parser mengembalikan-1. Ini melindungi calculator dari menerima nilai garbage. - Baris 51 (
return 0;): Karenaassertmemanggilabort()pada kegagalan, test runner tidak perlu mengembalikan kode error spesifik—jika sampai baris ini, semua test lulus.
Memilih Cakupan Integration Test #
Integration test tidak boleh menggandakan unit test. Jika parser sudah diuji menyeluruh di test unit (kasus valid, invalid, edge case), integration test hanya perlu memverifikasi " Antar-muka antar modul konsisten". Tiga pola cakupan umum:
- Contract test: Memverifikasi modul A menghasilkan data yang dapat langsung dikonsumsi modul B. Contoh di atas adalah contract test antara
parserdancalc. - End-to-end test: Memverifikasi seluruh pipeline dari input pengguna hingga output akhir. Cocok untuk aplikasi CLI atau server.
- Smoke test: Hanya menjalankan beberapa skenario dasar untuk memastikan sistem tidak rusak parah—biasanya dijalankan lebih cepat dari test lengkap.
Jebakan: Kontrak yang Tidak Tertulis #
Misalkan parser mengembalikan op berupa karakter 'X' (operator tidak dikenal) tanpa menetapkan kode error. Calculator kemudian menerima 'X', masuk ke cabang default, dan menetapkan err = 1. Pengguna akhir menerima error generik yang sulit dilacak akar masalahnya. Untuk mencegahnya, kita wajib menegakkan kontrak secara eksplisit:
- Parser wajib menolak operator yang tidak didukung calculator (test
test_parse_invalid_opdi atas menangkap ini). - Calculator wajib menetapkan
erruntuk semua path non-happy (testtest_parse_and_calc_div_zeromemverifikasi ini). - Tipe data antar modul harus konsisten—misalnya
intdi parser danintdi calculator, bukanlongdi salah satu.