Unit test biasa memverifikasi satu contoh input dan satu output yang diharapkan. Property-based testing membalik pendekatan ini: kita menyatakan properti atau invariant yang harus selalu benar—misalnya “setelah sort, array terurut menaik”—lalu menjalankan ratusan input acak untuk memverifikasi properti tersebut. Pendekatan ini sering menemukan bug yang luput dari test tradisional karena pikiran kita tidak memikirkan semua kombinasi input. Pada chapter ini kita menerapkan property-based testing pada fungsi int_sort() dengan dua properti: hasil tetap terurut, dan hasil tetap merupakan permutasi input.
Properti vs Contoh #
Perbedaan utama antara unit test dan property-based test adalah arah pernyataan:
- Unit test: “Input
[3, 1, 2]harus menghasilkan output[1, 2, 3].” - Property-based test: “Untuk semua input, hasil sort harus (a) terurut menaik dan (b) merupakan permutasi input.”
Property-based test tidak mengikat implementasi pada output tertentu—algoritma sort apa pun (insertion, quicksort, mergesort) lulus test selama kedua properti terpenuhi. Karena itu, property test ideal untuk refactor: kita bisa mengganti algoritma tanpa mengubah test.
Implementasi int_sort dan Dua Properti #
Kita mulai dengan implementasi insertion sort yang benar, lalu menulis property test yang menjalankan 200 trial acak. Setiap trial menghasilkan array dengan panjang acak dan elemen acak, mengurutkannya, lalu memeriksa kedua properti.
/* File: sort.h */
#ifndef SORT_H
#define SORT_H
#include <stddef.h>
void int_sort(int *arr, size_t n);
#endif/* File: sort.c — insertion sort (deterministik) */
#include "sort.h"
void int_sort(int *arr, size_t n) {
if (arr == NULL || n < 2) return;
for (size_t i = 1; i < n; i++) {
int key = arr[i];
size_t j = i;
while (j > 0 && arr[j - 1] > key) {
arr[j] = arr[j - 1];
j--;
}
arr[j] = key;
}
}/* File: test_property.c — property-based test */
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include "sort.h"
static bool is_sorted(const int *arr, size_t n) {
for (size_t i = 1; i < n; i++) {
if (arr[i] < arr[i - 1]) return false;
}
return true;
}
static bool is_permutation(const int *a, const int *b, size_t n) {
/* Cek multiset: hitung frekuensi nilai di a dan b */
for (size_t i = 0; i < n; i++) {
size_t count_a = 0, count_b = 0;
for (size_t j = 0; j < n; j++) {
if (a[j] == a[i]) count_a++;
if (b[j] == a[i]) count_b++;
}
if (count_a != count_b) return false;
}
return true;
}
int main(void) {
srand(42); /* seed deterministik untuk reproduksibilitas */
size_t total_trials = 200;
size_t max_n = 50;
int failures = 0;
for (size_t trial = 0; trial < total_trials; trial++) {
size_t n = (size_t)(rand() % max_n) + 1;
int *orig = malloc(n * sizeof(int));
int *sorted = malloc(n * sizeof(int));
if (orig == NULL || sorted == NULL) {
fprintf(stderr, "OOM\n");
return 2;
}
for (size_t i = 0; i < n; i++) {
orig[i] = rand() % 100; /* rentang [0, 99] */
sorted[i] = orig[i];
}
int_sort(sorted, n);
/* Properti 1: hasil sorted */
if (!is_sorted(sorted, n)) {
printf("FAIL trial %zu: bukan sorted\n", trial);
failures++;
}
/* Properti 2: hasil adalah permutasi input */
if (!is_permutation(orig, sorted, n)) {
printf("FAIL trial %zu: bukan permutasi\n", trial);
failures++;
}
free(orig);
free(sorted);
}
printf("Total trials: %zu, Failures: %d\n", total_trials, failures);
return failures != 0;
}Build dan jalankan:
gcc -std=c11 -Wall -Wextra -o test_property test_property.c sort.c && ./test_propertyOutput yang Dihasilkan:
Total trials: 200, Failures: 0Mari kita telaah baris-baris kunci di atas:
- Baris 6-11 (
is_sorted): Fungsi property checker yang memverifikasi array terurut menaik. Loop membandingkan setiap pasangan berurutan—bila adaarr[i] < arr[i-1], properti gagal. Fungsi ini tidak tahu algoritma sort yang dipakai, hanya mengetahui properti yang diharapkan. - Baris 13-25 (
is_permutation): Fungsi property checker kedua. Untuk setiap nilaia[i], fungsi menghitung berapa kali nilai muncul diadan dib. Bila jumlah berbeda,bbukan permutasia. Implementasi ini O(n²)—cukup untuk test kecil, tetapi untuk array besar gunakan hash table. - Baris 28 (
srand(42);): Seed deterministik penting agar test dapat direproduksi. Tanpa seed tetap, kegagalan yang muncul di CI mungkin tidak terulang di lokal—debugging menjadi sangat sulit. - Baris 36-37 (
origdansorted): Kita menyimpan dua salinan array:origuntuk verifikasi permutasi,sorteduntuk diurutkan dan diverifikasi. Tanpaorig, kita tidak bisa membuktikan bahwa sort mempertahankan elemen. - Baris 47-58 (pemeriksaan properti): Kedua properti diperiksa secara independen. Implementasi sort yang salah mungkin melanggar satu properti tetapi memenuhi yang lain. Misalnya, implementasi yang mengembalikan
arrtanpa diurutkan akan lulus properti permutasi tetapi gagal properti sorted. - Baris 67 (
return failures != 0;): Kode exit non-zero bila ada kegagalan. CI mendeteksi kegagalan lewat kode exit, bukan lewat parsing output.
Mendeteksi Bug dengan Property Test #
Kekuatan property test terlihat saat kita mengganti implementasi int_sort dengan versi buggy—misalnya hanya melakukan satu pass bubble tanpa loop luar. Property test menemukan kegagalan di trial pertama karena array acak hampir selalu tidak terurut setelah satu pass.
/* File: sort_buggy.c — implementasi buggy (satu pass bubble) */
#include "sort.h"
/* BUG: hanya sekali pass — tidak mengurutkan penuh */
void int_sort(int *arr, size_t n) {
if (arr == NULL || n < 2) return;
for (size_t i = 1; i < n; i++) {
if (arr[i - 1] > arr[i]) {
int tmp = arr[i - 1];
arr[i - 1] = arr[i];
arr[i] = tmp;
}
}
}Test runner yang sama (dengan break setelah 3 kegagalan agar output ringkas) menghasilkan:
FAIL trial 0 (n=17): bukan sorted
FAIL trial 1 (n=49): bukan sorted
FAIL trial 2 (n=21): bukan sorted
Total trials: 200, Failures: 3Implementasi buggy gagal di tiga trial pertama berturut-turut. Tanpa property test, kita harus menulis puluhan unit test dengan input manual untuk mencapai cakupan setara. Property test mengompres pekerjaan tersebut ke loop tunggal.
Memilih Properti yang Bermakna #
Properti yang baik adalah pernyataan universal yang:
- Tidak bergantung pada algoritma internal: “Hasil sort terurut” lulus untuk semua algoritma sort yang benar.
- Dapat diuji tanpa mengulang implementasi: Jangan menulis properti yang pada dasarnya mengulang kode produksi—misalnya “sort membandingkan pasangan
arr[j-1] > arr[j]”. Properti seperti ini tidak menangkap bug logika karena sama dengan kode yang diuji. - Cocok dengan domain: Untuk fungsi keuangan, properti bisa berupa “total transaksi = total kredit + total debit”. Untuk parser, properti bisa berupa “parse(inverse_parse(x)) == x”.