Seluruh build system yang kita rakit selama ini menghasilkan satu hal: program. Tetapi program yang berhenti saat selesai di-build belum tentu program yang benar — dan memastikan yang benar tetap benar setelah setiap perubahan bukan pekerjaan yang sehat dilakukan manual. Uji coba berulang adalah pekerjaan mesin: program kecil yang memanggil fungsi kita, membandingkan hasilnya dengan nilai yang diharapkan, lalu melaporkan lulus atau gagal. CTest adalah pelari tes (test runner) bawaan CMake yang mengelola kumpulan program tes seperti itu — dan pada Chapter ini kita membangun versi mininya sungguhan dengan bash, supaya mekanisme pass/fail-nya terlihat sampai ke akarnya.

Anatomi Program Tes #

Program tes bukan program biasa — ia adalah program yang keluarannya hanya punya dua makna:

  • Exit code 0 — semua kasus yang diperiksa sesuai harapan: tes lulus.
  • Exit code selain 0 — ada kasus yang menyimpang: tes gagal.

Bentuk paling murninya tidak butuh framework apa pun. Berikut program tes untuk fungsi perhitungan diskon — setiap kasus menyimpan input beserta harapannya, dan program berhenti dengan exit code 1 begitu menemukan ketidakcocokan:

cpp
// Tes untuk fungsi diskon: setiap kasus memeriksa satu harapan.
#include <iostream>

int hargaSetelahDiskon(int harga, int persen) {
    return harga - harga * persen / 100;
}

int main() {
    struct Kasus {
        int harga;
        int persen;
        int harapan;
    };

    Kasus kasus[] {
        {10000, 25, 7500},
        {200000, 50, 100000},
        {15000, 0, 15000}
    };

    for (const Kasus& k : kasus) {
        int hasil = hargaSetelahDiskon(k.harga, k.persen);
        if (hasil != k.harapan) {
            std::cout << "GAGAL - " << k.harga << " diskon " << k.persen
                      << "% -> " << hasil << ", harapan " << k.harapan << "\n";
            return 1;
        }
        std::cout << "ok    - " << k.harga << " diskon " << k.persen
                  << "% -> " << hasil << "\n";
    }
    std::cout << "3 kasus lulus.\n";
    return 0;
}

Output yang Dihasilkan:

text
ok    - 10000 diskon 25% -> 7500
ok    - 200000 diskon 50% -> 100000
ok    - 15000 diskon 0% -> 15000
3 kasus lulus.

Polanya selalu sama: siapkan tabel kasus, jalankan fungsi yang diuji untuk setiap barisnya, bandingkan dengan harapan. Pembandingan ini yang kelak diotomatiskan framework pengujian seperti GoogleTest — tetapi kontraknya tidak pernah berubah: program tes diadili dari exit code-nya.

Sekarang sisi lain dari kontrak itu — program tes yang gagal. Versi kedua sengaja kita beri kasus ketiga yang salah (string "matahari" punya 8 huruf, bukan 5):

cpp
// Tes yang SENGAJA gagal pada kasus ketiga, untuk melihat reaksi test runner.
#include <iostream>
#include <string>

int panjangTerpanjang(const std::string& a, const std::string& b) {
    return (a.size() >= b.size()) ? static_cast<int>(a.size())
                                  : static_cast<int>(b.size());
}

int main() {
    struct Kasus {
        std::string a;
        std::string b;
        int harapan;
    };

    Kasus kasus[] {
        {"kucing", "sapi", 6},
        {"sepeda", "motor", 6},
        {"matahari", "bulan", 5}   // salah: "matahari" punya 8 huruf
    };

    for (const Kasus& k : kasus) {
        int hasil = panjangTerpanjang(k.a, k.b);
        if (hasil == k.harapan) {
            std::cout << "ok    - " << k.a << " vs " << k.b << "\n";
        } else {
            std::cout << "GAGAL - " << k.a << " vs " << k.b
                      << ": harapan " << k.harapan << ", hasil " << hasil << "\n";
            return 1;
        }
    }
    return 0;
}

Output yang Dihasilkan:

text
ok    - kucing vs sapi
ok    - sepeda vs motor
GAGAL - matahari vs bulan: harapan 5, hasil 8

Program ini berhenti di kasus ketiga dengan exit code 1 — dan justru baris GAGAL plus kode keluar itulah yang membuat pelari tes tahu harus melaporkan apa.

Runner Mini yang Dijalankan Sungguhan #

Tugas pelari tes sebenarnya sederhana: jalankan setiap program tes, baca exit code-nya, laporkan. Skrip bash berikut melakukannya untuk semua program bernama tes-*.out di folder yang sama:

bash
#!/usr/bin/env bash
gagal=0
for tes in ./tes-*.out; do
    "$tes"
    status=$?
    if [ "$status" -eq 0 ]; then
        echo "[LULUS] $tes"
    else
        echo "[GAGAL] $tes (exit code $status)"
        gagal=$((gagal + 1))
    fi
done

if [ "$gagal" -eq 0 ]; then
    echo "Semua tes lulus."
    exit 0
fi
echo "$gagal dari 2 tes gagal."
exit 1

Tiga hal layak dicermati. Loop for tes in ./tes-*.out memanfaatkan pola glob sehingga tes baru tidak perlu didaftarkan. Perintah "$tes" menjalankan programnya, lalu $? — kode keluar perintah terakhir — disimpan ke status sebelum dipakai. Dan skrip ini sendiri patuh pada kontrak yang sama: exit code 0 bila semua tes lulus, 1 bila tidak, sehingga runner bisa dipanggil runner lain. Inilah log yang nyata dari lingkungan praktikum, dari kompilasi sampai pelaporan — termasuk echo $? yang menunjukkan exit code runner:

bash
g++ -std=c++23 -Wall -Wextra tes-diskon.cpp -o tes-diskon.out
g++ -std=c++23 -Wall -Wextra tes-kata.cpp -o tes-kata.out
./jalankan-tes.sh
echo $?

Output yang Dihasilkan:

text
ok    - 10000 diskon 25% -> 7500
ok    - 200000 diskon 50% -> 100000
ok    - 15000 diskon 0% -> 15000
3 kasus lulus.
[LULUS] ./tes-diskon.out
ok    - kucing vs sapi
ok    - sepeda vs motor
GAGAL - matahari vs bulan: harapan 5, hasil 8
[GAGAL] ./tes-kata.out (exit code 1)
1 dari 2 tes gagal.
1

Pelari mengeksekusi kedua program secara berurutan, menyisipkan vonis [LULUS]/[GAGAL] di antara output masing-masing, lalu mengutip jumlah kegagalan — dan echo $? mengonfirmasi exit code 1 menular dari runner ke shell. Membetulkan kasus ketiga di tes-kata.cpp lalu menjalankan ulang skrip adalah latihan yang dianjurkan: satu-satunya perubahan yang dibutuhkan runner adalah tidak sama sekali.

Kembali ke CTest #

CTest mengelola daftar tes dengan cara yang sama, hanya saja daftarnya dinyatakan di CMakeLists.txt:

cmake CMakeLists.txt (potongan)
enable_testing()

add_executable(tes-diskon tes-diskon.cpp)
add_executable(tes-kata tes-kata.cpp)

add_test(NAME diskon COMMAND tes-diskon)
add_test(NAME kata COMMAND tes-kata)

enable_testing() menyiapkan proyek untuk pengujian dan wajib dipanggil di CMakeLists.txt teratas; add_test mendaftarkan satu tes — NAME adalah label pelaporannya, COMMAND program yang dijalankan beserta argumennya bila ada. Sesudah build, perintah ctest dijalankan dari folder build dan mengadili setiap tes dari exit code-nya, persis skrip mini kita. Keluarannya satu baris per tes — nomor urut, nama, dan status Passed/Failed — dengan ringkasan jumlah di akhir. Opsi yang paling sering dipakai:

bash
ctest                      # jalankan semua tes
ctest --output-on-failure  # tampilkan output lengkap tes yang gagal
ctest -R diskon            # hanya tes yang namanya cocok pola
ctest -j 4                 # jalankan empat tes secara paralel

Yang belum kita jawab adalah pertanyaan sebelumnya di rantai build: program yang diuji itu dibangun dengan konfigurasi apa? Kompilasi debug atau release ternyata bukan hanya soal kecepatan — ia mengubah perilaku program yang sama. Itulah topik chapter berikutnya.

Referensi: CTest , GoogleTest primer .