Bug yang pernah diperbaiki memiliki kecenderungan untuk kembali—seorang pengembang melakukan refactor tiga bulan kemudian, tidak menyadari bahwa perubahan kecil mereka menghidupkan kembali bug lama. Regression test adalah jaring pengaman terhadap situasi ini. Setiap kali kita memperbaiki bug, kita menulis test yang mereproduksi bug tersebut. Test ini ditambahkan ke regression suite yang berjalan di setiap commit, sehingga bug yang sama tidak akan muncul kembali tanpa terdeteksi. Pada chapter ini kita melihat contoh bug #142 pada implementasi stack, menulis regression test untuk bug tersebut, lalu memverifikasi bahwa versi buggy lama akan tertangkap oleh test itu.
Anatomi Bug yang Akan Diregression-Test #
Misalkan kita memiliki modul Stack sederhana dengan operasi stack_push dan stack_pop. Bug #142 dilaporkan: ketika stack_pop dipanggil pada stack kosong, fungsi membaca arr[-1] (underflow) alih-alih mengembalikan indikator error. Berikut implementasi buggy dan fixed:
/* File: stack.h */
#ifndef STACK_H
#define STACK_H
#include <stddef.h>
#include <stdbool.h>
typedef struct Stack Stack;
Stack *stack_new(size_t cap);
void stack_free(Stack *s);
bool stack_push(Stack *s, int v);
bool stack_pop(Stack *s, int *out);
size_t stack_size(const Stack *s);
#endif/* File: stack.c — implementasi dengan FIX bug #142 */
#include "stack.h"
#include <stdlib.h>
struct Stack {
int *arr;
size_t cap;
size_t top;
};
Stack *stack_new(size_t cap) {
Stack *s = malloc(sizeof(*s));
if (s == NULL) return NULL;
s->arr = malloc(cap * sizeof(int));
if (s->arr == NULL) { free(s); return NULL; }
s->cap = cap;
s->top = 0;
return s;
}
void stack_free(Stack *s) {
if (s == NULL) return;
free(s->arr);
free(s);
}
bool stack_push(Stack *s, int v) {
if (s == NULL) return false;
if (s->top >= s->cap) return false; /* full */
s->arr[s->top++] = v;
return true;
}
bool stack_pop(Stack *s, int *out) {
if (s == NULL || out == NULL) return false;
if (s->top == 0) return false; /* empty — FIX BUG #142 */
*out = s->arr[--s->top];
return true;
}
size_t stack_size(const Stack *s) {
return (s != NULL) ? s->top : 0;
}Regression Test untuk Bug #142 #
Regression test berikut menyertakan komentar yang menjelaskan bug asal. Test menyiapkan kondisi yang memicu bug (stack kosong), lalu memverifikasi bahwa implementasi yang sudah diperbaiki mengembalikan false dan tidak mengubah variabel output.
/* File: test_stack.c — regression test untuk bug #142 */
#include <stdio.h>
#include <assert.h>
#include "stack.h"
static int tests_run = 0;
static int tests_failed = 0;
#define CHECK(cond) \
do { \
tests_run++; \
if (!(cond)) { \
tests_failed++; \
printf("FAIL line %d: %s\n", __LINE__, #cond); \
} \
} while (0)
int main(void) {
Stack *s = stack_new(3);
CHECK(s != NULL);
/* Kasus normal */
CHECK(stack_push(s, 10) == true);
CHECK(stack_push(s, 20) == true);
CHECK(stack_push(s, 30) == true);
CHECK(stack_push(s, 40) == false); /* penuh */
CHECK(stack_size(s) == 3);
int v;
CHECK(stack_pop(s, &v) == true && v == 30);
CHECK(stack_pop(s, &v) == true && v == 20);
CHECK(stack_pop(s, &v) == true && v == 10);
CHECK(stack_size(s) == 0);
/* === Regression test untuk bug #142 ===
* Sebelumnya, stack_pop pada stack kosong membaca arr[-1]
* (underflow). Test ini memastikan stack_pop mengembalikan
* false dan tidak mengubah *out. */
int sentinel = 999;
int out = sentinel;
CHECK(stack_pop(s, &out) == false);
CHECK(out == sentinel); /* tidak diubah */
stack_free(s);
printf("Total: %d, Failed: %d\n", tests_run, tests_failed);
return tests_failed != 0;
}Build dan jalankan:
gcc -std=c11 -Wall -Wextra -o test_stack test_stack.c stack.c && ./test_stackOutput yang Dihasilkan:
Total: 12, Failed: 0Mari kita telaah baris-baris kunci di atas:
- Baris 17-22 (
stack_popdenganif (s->top == 0) return false;): Baris ini adalah fix untuk bug#142. Tanpa baris ini, ekspresi--s->topmengurangis->topdari0menjadi(size_t)-1(karenatopbertipesize_tyang unsigned), lalu membacaarr[(size_t)-1]—lokasi sebelum awal array. - Baris 47-52 (regression test): Test menyiapkan stack kosong dengan
stack_size(s) == 0. Variabeloutdiisi nilai sentinel999—nilai yang tidak mungkin muncul sebagai data stack sebenarnya. Setelahstack_pop,outharus tetap999karena fungsi yang benar tidak menulis keoutbila tidak ada elemen untuk di-pop. - Baris 49 (
CHECK(stack_pop(s, &out) == false);): Memverifikasi bahwastack_popmengembalikanfalse—indikator kegagalan. Implementasi buggy mengembalikantruemeskipun stack kosong, sehingga test ini akan gagal. - Baris 50 (
CHECK(out == sentinel);): Memverifikasi bahwaouttidak diubah olehstack_pop. Implementasi buggy menulis keoutsetelah membacaarr[-1], sehinggaoutakan berisi nilai garbage, bukan999. - Komentar
=== Regression test untuk bug #142 ===: Komentar ini menjelaskan asal test. Pengembang yang membaca test tiga bulan kemudian langsung mengerti kenapa test ini ada—mengacu pada bug tracker#142.
Memverifikasi Bahwa Bug Tertangkap #
Regression test hanya berguna bila benar-benar menangkap bug asli. Kita dapat memverifikasi ini dengan sementara mengembalikan versi buggy dari stack_pop dan menjalankan test—test harus gagal. Berikut versi buggy:
/* File: stack_buggy.c — BUG #142: stack_pop pada stack kosong */
#include "stack.h"
#include <stdlib.h>
struct Stack {
int *arr;
size_t cap;
size_t top;
};
/* (implementasi lain identik dengan stack.c) */
bool stack_pop(Stack *s, int *out) {
if (s == NULL || out == NULL) return false;
/* BUG: tidak cek top == 0 */
*out = s->arr[--s->top]; /* underflow bila top == 0 */
return true;
}Build test runner yang sama dengan stack_buggy.c dan ASan untuk membuktikan adanya akses memori ilegal:
gcc -std=c11 -Wall -Wextra -fsanitize=address \
-o test_stack_buggy test_stack.c stack_buggy.c && ./test_stack_buggyOutput yang Dihasilkan (dipotong untuk ringkas):
=================================================================
==9533==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x50200000000c
READ of size 4 at 0x50200000000c thread T0
#0 0x... in stack_pop (/tmp/test_regression/test_stack_buggy+0x1bd0)
#1 0x... in main (/tmp/test_regression/test_stack_buggy+0x1703)
0x50200000000c is located 4 bytes before 12-byte region [0x502000000010,0x50200000001c)
allocated by thread T0 here:
#0 0x... in malloc
#1 0x... in stack_new
SUMMARY: AddressSanitizer: heap-buffer-overflow in stack_popASan menunjukkan READ of size 4 pada alamat yang berada 4 bytes before wilayah arr—tepat membaca arr[-1]. Tanpa regression test, bug ini bisa kapan saja muncul kembali bila pengembang lain secara tidak sengaja menghapus baris if (s->top == 0) return false; saat refactor.
Strategi Menulis Regression Test #
Beberapa pedoman agar regression test efektif:
- Tulis test sebelum fix. Idealnya, saat bug dilaporkan, tulis test yang mereproduksinya (test Red). Setelah fix diterapkan, test berubah menjadi Green—dan menjadi regression test permanen.
- Test harus minimal. Hanya sertakan langkah yang diperlukan untuk memicu bug. Hindari setup kompleks yang membuat test rapuh terhadap perubahan tidak terkait.
- Komentari asal bug. Cantumkan ID bug tracker (
#142,GH-789) atau tautan commit yang memperbaiki bug. Komentar ini menjelaskan kenapa test ada—bukan apa yang diuji. - Gabungkan dengan sanitizer. Beberapa bug (underflow, use-after-free) tidak crash tanpa ASan. Jalankan regression suite dengan
-fsanitize=address,undefineduntuk meningkatkan deteksi.