Bug paling sering muncul bukan di tengah rentang input, melainkan di tepi—saat nilai mencapai minimum, maksimum, nol, atau satu langkah di luarnya. Konsep ini dikenal sebagai boundary value analysis. Sebuah fungsi clamp_value(v, lo, hi) mungkin lulus test dengan input (5, 0, 10), tetapi gagal saat v persis sama dengan lo, satu di bawah lo, atau saat lo dan hi adalah INT_MIN dan INT_MAX. Pada chapter ini kita berfokus pada lima kategori boundary: batas bawah, batas atas, off-by-one, limit integer, dan array index out-of-bounds.

Lima Kategori Boundary yang Wajib Diuji #

Untuk setiap fungsi yang menerima nilai numerik atau index, kita perlu menguji:

  1. Batas bawah: nilai terkecil yang valid (misalnya 0 untuk index, INT_MIN untuk int).
  2. Batas atas: nilai terbesar yang valid (misalnya n-1 untuk index, INT_MAX untuk int).
  3. Off-by-one bawah: nilai satu langkah di bawah batas bawah valid.
  4. Off-by-one atas: nilai satu langkah di atas batas atas valid.
  5. Kasus tepi khusus: nol, satu elemen, array kosong, NULL pointer, atau SIZE_MAX untuk size_t.

Contoh: Fungsi clamp_value #

clamp_value(v, lo, hi) membatasi v ke rentang [lo, hi]. Boundary yang harus diuji mencakup v == lo, v == hi, v < lo, v > hi, hingga limit INT_MIN/INT_MAX. Berikut implementasi dan test-nya:

c
/* File: clamp.c — fungsi clamp_value + test boundary */
#include <stdio.h>
#include <limits.h>

int clamp_value(int v, int lo, int hi) {
    /* Precondition: lo <= hi */
    if (lo > hi) return lo; /* fallback defensif */
    if (v < lo) return lo;
    if (v > hi) return hi;
    return v;
}

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) {
    /* Kasus normal */
    CHECK(clamp_value(5, 0, 10) == 5);

    /* Boundary bawah */
    CHECK(clamp_value(-1, 0, 10) == 0);     /* dipotong ke bawah */
    CHECK(clamp_value(0, 0, 10) == 0);      /* persis batas bawah */

    /* Boundary atas */
    CHECK(clamp_value(11, 0, 10) == 10);    /* dipotong ke atas */
    CHECK(clamp_value(10, 0, 10) == 10);    /* persis batas atas */

    /* Tepat di tengah */
    CHECK(clamp_value(5, 5, 5) == 5);       /* lo == hi == v */

    /* Limit INT_MAX dan INT_MIN */
    CHECK(clamp_value(INT_MAX, 0, 100) == 100);
    CHECK(clamp_value(INT_MIN, 0, 100) == 0);
    CHECK(clamp_value(INT_MAX, INT_MIN, INT_MAX) == INT_MAX);
    CHECK(clamp_value(INT_MIN, INT_MIN, INT_MAX) == INT_MIN);

    /* Off-by-one: nilai +1 / -1 dari batas */
    CHECK(clamp_value(99, 0, 100) == 99);
    CHECK(clamp_value(101, 0, 100) == 100);
    CHECK(clamp_value(-1, 0, 100) == 0);
    CHECK(clamp_value(1, 0, 100) == 1);

    printf("Total: %d, Failed: %d\n", tests_run, tests_failed);
    return tests_failed != 0;
}

Output yang Dihasilkan:

text
Total: 14, Failed: 0

Mari kita telaah baris-baris kunci di atas:

  1. Baris 6-7 (if (lo > hi) return lo;): Implementasi mengandung defensive fallback untuk kasus lo > hi. Tanpa fallback, fungsi akan mengembalikan v tanpa memotong—bug halus yang hanya muncul ketika pemanggil salah passing argumen.
  2. Baris 8 (if (v < lo) return lo;): Pemotongan ke batas bawah. Operator < (bukan <=) memastikan v == lo tidak dianggap kasus pemotongan—v dikembalikan tanpa modifikasi.
  3. Baris 27-31 (boundary bawah): Test clamp_value(-1, 0, 10) == 0 memverifikasi off-by-one bawah, sementara clamp_value(0, 0, 10) == 0 memverifikasi nilai persis di batas. Kedua test bersama menegaskan bahwa pemotongan terjadi di nilai < lo, bukan <= lo.
  4. Baris 39 (clamp_value(INT_MAX, 0, 100) == 100): Boundary dengan INT_MAX memastikan tidak ada overflow saat membandingkan. Implementasi tidak melakukan operasi aritmatika pada v—hanya pembandingan—sehingga aman dari overflow.
  5. Baris 41-42 (limit rentang): Test clamp_value(INT_MAX, INT_MIN, INT_MAX) dan clamp_value(INT_MIN, INT_MIN, INT_MAX) memverifikasi bahwa fungsi bekerja dengan rentang terluas mungkin. Tanpa test ini, bug seperti if (v + 1 < hi) (yang overflow saat v == INT_MAX) tidak akan terdeteksi.
  6. Baris 45-48 (off-by-one dari batas 100): Empat test ini menegaskan bahwa 99, 100, 101, dan 1 semuanya dikembalikan atau dipotong dengan benar. Test ini menangkap bug seperti if (v > hi) yang seharusnya if (v >= hi)—perbedaan satu karakter yang sering luput.

Boundary Index Array #

Bug off-by-one pada index array lebih berbahaya karena tidak terlihat hingga runtime. Berikut fungsi array_get_safe yang mengembalikan nilai default bila index out-of-range, beserta test boundary yang mencakup index valid, index n (off-by-one atas), array kosong, NULL, dan SIZE_MAX:

c
/* File: array_idx.c — boundary test untuk array indexing */
#include <stdio.h>
#include <stddef.h>

/* Akses aman: kembalikan default bila index out-of-range */
int array_get_safe(const int *arr, size_t n, size_t idx, int def) {
    if (arr == NULL) return def;
    if (idx >= n) return def;
    return arr[idx];
}

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) {
    int arr[] = {10, 20, 30, 40, 50};
    size_t n = sizeof(arr) / sizeof(arr[0]);

    /* Index valid */
    CHECK(array_get_safe(arr, n, 0, -1) == 10);
    CHECK(array_get_safe(arr, n, n - 1, -1) == 50);

    /* Off-by-one: index == n (out of bounds) */
    CHECK(array_get_safe(arr, n, n, -1) == -1);
    /* Index == n + 1 */
    CHECK(array_get_safe(arr, n, n + 1, -1) == -1);

    /* Boundary nol: array kosong */
    CHECK(array_get_safe(arr, 0, 0, -1) == -1);

    /* NULL array */
    CHECK(array_get_safe(NULL, n, 0, -1) == -1);

    /* Index SIZE_MAX (overflow boundary) */
    CHECK(array_get_safe(arr, n, (size_t)-1, -1) == -1);

    printf("Total: %d, Failed: %d\n", tests_run, tests_failed);
    return tests_failed != 0;
}

Output yang Dihasilkan:

text
Total: 7, Failed: 0

Implementasi array_get_safe menggunakan operator >= (bukan >) sehingga index n dianggap out-of-range—sesuai konvensi C bahwa index valid adalah 0 .. n-1. Test (size_t)-1 setara dengan SIZE_MAX dan memastikan tidak ada overflow saat membandingkan idx >= n. Karena idx bertipe size_t (unsigned), pembandingan aman dari underflow.

Strategi Memilih Boundary #

Untuk fungsi baru, kita dapat memilih boundary dengan mengikuti tabel konseptual berikut:

Tipe inputBoundary yang diuji
int0, 1, -1, INT_MAX, INT_MIN, INT_MAX - 1
size_t0, 1, n-1, n, SIZE_MAX
char *NULL, "", string 1 karakter, string panjang maks.
float/double0.0, -0.0, INFINITY, NAN, DBL_MIN, DBL_MAX