Unit test menguji satu unit kode — biasanya satu fungsi atau satu kelas — secara terisolasi dari sistem lain. Untuk kode DSA, unit test memberi jaring pengaman ketika kita refactor algoritma atau mengganti struktur data internal. Pada Chapter ini kita akan membahas dua framework populer di ekosistem C++ (GoogleTest dan Catch2), kemudian membangun mini test framework ala GoogleTest yang dapat dikompilasi tanpa dependensi eksternal, lengkap dengan macro TEST, EXPECT_EQ, ASSERT_EQ, dan fixture pattern. Tujuan akhirnya: kita dapat menulis ratusan test case tanpa pustaka tambahan, sambil memahami konsep yang sama dipakai oleh GoogleTest di industri.

Dua Framework Populer #

FrameworkLisensiKhasCocok untuk
GoogleTest (gtest)BSD-3Sintaks TEST(), TEST_F, EXPECT_*, ASSERT_*Proyek produksi C++ skala besar
Catch2BSL-1.0Single-header, TEST_CASE, REQUIRE, SECTIONProyek kecil, header-only
doctestMITMirip Catch2 tetapi lebih cepat compileAlternatif Catch2

Kedua framework menyediakan dua kategori assertion:

  • EXPECT_*: mencatat kegagalan tetapi test tetap berlanjut. Cocok untuk pemeriksaan non-kritis.
  • ASSERT_*: mencatat kegagalan dan segera menghentikan test. Cocok bila langkah selanjutnya tidak bermakna bila precondition gagal.

Mekanisme: Bagaimana Macro TEST Bekerja #

Di balik layar, TEST(Suite, Name) membuat fungsi unik dengan nama mangled (misalnya VectorTest_ReverseEmpty_impl), kemudian mendaftarkan fungsi tersebut ke global registry via static initializer. Saat main dijalankan, framework iterasi registry dan mengeksekusi setiap test. Setiap test melaporkan hasil ke global result list, lalu main mencetak ringkasan.

Karena kita tidak punya gtest, kita akan meniru pola ini dengan mini framework — cukup untuk mendemonstrasikan konsepnya secara end-to-end.

Kode: Mini Framework Ala GoogleTest #

cpp
#include <iostream>
#include <vector>
#include <string>
#include <sstream>
#include <functional>

// ---------- Mini framework ----------
struct TestResult {
    std::string name;
    bool ok;
    std::string reason;
};

static std::vector<TestResult> g_results;
static std::string g_current_test;
static std::ostringstream g_reason;
static bool g_failed = false;

#define TEST(suite, name)                                                     \
    static void suite##_##name##_impl();                                       \
    static bool reg_##suite##_##name = []() {                                  \
        g_results.push_back({#suite "." #name, false, ""});                    \
        suite##_##name##_impl();                                              \
        g_results.back().ok = !g_failed;                                      \
        g_results.back().reason = g_reason.str();                             \
        g_failed = false; g_reason.str("");                                   \
        return true;                                                          \
    }();                                                                       \
    static void suite##_##name##_impl()

#define EXPECT_EQ(a, b)                                                       \
    do {                                                                       \
        auto _va = (a); auto _vb = (b);                                        \
        if (!(_va == _vb)) {                                                   \
            g_failed = true;                                                  \
            g_reason << "EXPECT_EQ failed: " #a " != " #b                      \
                     << " (line " << __LINE__ << ")\n";                       \
        }                                                                      \
    } while (0)

#define ASSERT_EQ(a, b)                                                       \
    do {                                                                       \
        auto _va = (a); auto _vb = (b);                                        \
        if (!(_va == _vb)) {                                                   \
            g_failed = true;                                                  \
            g_reason << "ASSERT_EQ failed: " #a " != " #b                      \
                     << " (line " << __LINE__ << ")\n";                        \
            return;                                                            \
        }                                                                      \
    } while (0)

// ---------- Code under test ----------
std::vector<int> reverse_vec(const std::vector<int>& a) {
    return std::vector<int>(a.rbegin(), a.rend());
}

bool is_sorted_asc(const std::vector<int>& a) {
    for (size_t i = 1; i < a.size(); ++i)
        if (a[i-1] > a[i]) return false;
    return true;
}

struct Stack {
    std::vector<int> data;
    void push(int x) { data.push_back(x); }
    int pop() { int v = data.back(); data.pop_back(); return v; }
    bool empty() const { return data.empty(); }
    size_t size() const { return data.size(); }
};

// ---------- Test cases ----------
TEST(VectorTest, ReverseEmpty) {
    auto r = reverse_vec({});
    EXPECT_EQ(r.size(), (size_t)0);
}

TEST(VectorTest, ReverseOne) {
    auto r = reverse_vec({42});
    ASSERT_EQ(r.size(), (size_t)1);
    EXPECT_EQ(r[0], 42);
}

TEST(VectorTest, ReverseThree) {
    auto r = reverse_vec({1, 2, 3});
    ASSERT_EQ(r.size(), (size_t)3);
    EXPECT_EQ(r[0], 3);
    EXPECT_EQ(r[1], 2);
    EXPECT_EQ(r[2], 1);
}

TEST(SortTest, EmptyIsSorted) {
    EXPECT_EQ(is_sorted_asc({}), true);
}

TEST(SortTest, SortedStaysTrue) {
    EXPECT_EQ(is_sorted_asc({1, 2, 2, 5}), true);
}

TEST(SortTest, UnsortedDetected) {
    EXPECT_EQ(is_sorted_asc({1, 3, 2}), false);
}

TEST(StackTest, PushPopOrder) {
    Stack s;
    s.push(10); s.push(20); s.push(30);
    ASSERT_EQ(s.size(), (size_t)3);
    EXPECT_EQ(s.pop(), 30);
    EXPECT_EQ(s.pop(), 20);
    EXPECT_EQ(s.pop(), 10);
    EXPECT_EQ(s.empty(), true);
}

TEST(StackTest, EmptyStack) {
    Stack s;
    EXPECT_EQ(s.size(), (size_t)0);
    EXPECT_EQ(s.empty(), true);
}

int main() {
    int pass = 0, fail = 0;
    for (const auto& r : g_results) {
        std::cout << (r.ok ? "[PASS] " : "[FAIL] ") << r.name;
        if (!r.ok) std::cout << "  --  " << r.reason;
        std::cout << '\n';
        r.ok ? pass++ : fail++;
    }
    std::cout << "\nSummary: " << pass << " passed, " << fail << " failed, "
              << g_results.size() << " total.\n";
    return fail == 0 ? 0 : 1;
}

Output yang Dihasilkan:

text
[PASS] VectorTest.ReverseEmpty
[PASS] VectorTest.ReverseOne
[PASS] VectorTest.ReverseThree
[PASS] SortTest.EmptyIsSorted
[PASS] SortTest.SortedStaysTrue
[PASS] SortTest.UnsortedDetected
[PASS] StackTest.PushPopOrder
[PASS] StackTest.EmptyStack

Summary: 8 passed, 0 failed, 8 total.

Bedah Kode Baris-per-Baris #

  1. static std::vector<TestResult> g_results;: registry global yang menyimpan hasil semua test. Setiap test akan memiliki satu entri di sini.
  2. static bool g_failed;: flag yang di-set true bila EXPECT_EQ atau ASSERT_EQ gagal. Di-reset di akhir setiap test.
  3. #define TEST(suite, name) ...: macro yang menghasilkan dua hal — fungsi impl dengan nama unik (VectorTest_ReverseEmpty_impl) dan static boolean (reg_VectorTest_ReverseEmpty). Static boolean diinisialisasi dengan lambda yang mengeksekusi test dan menyimpan hasil. Inilah yang mendaftarkan test ke registry.
  4. #define EXPECT_EQ(a, b): membandingkan a dan b dengan operator==. Bila tidak sama, set g_failed = true dan catat pesan ke g_reason. Eksekusi test tetap berlanjut.
  5. #define ASSERT_EQ(a, b): sama dengan EXPECT_EQ, tetapi bila gagal melakukan return; untuk keluar dari fungsi test seketika. Wajib untuk precondition.
  6. ASSERT_EQ(s.size(), (size_t)3);: di StackTest.PushPopOrder, kita pastikan stack berisi 3 elemen sebelum pop(). Bila tidak, akses ke data.back() akan undefined behavior.
  7. r.ok ? pass++ : fail++;: di main, hitung jumlah pass/fail. Exit code 1 bila ada failure — penting untuk CI.

Fixture Pattern #

GoogleTest menyediakan TEST_F (F = fixture) untuk setup/teardown berulang. Fixture memungkinkan kita menginisialisasi struktur umum satu kali per test tanpa duplikasi kode. Mini framework kita belum mendukungnya secara langsung, tetapi pola yang setara dapat ditulis dengan membuat struct berisi state + method setup():

cpp
struct GraphFixture {
    std::vector<std::vector<int>> adj;
    void setup(int n) { adj.assign(n, {}); }
    void add_edge(int u, int v) { adj[u].push_back(v); }
};

TEST_F_PATTERN(GraphTest, ThreeNodes) {
    GraphFixture f;
    f.setup(3);
    f.add_edge(0, 1);
    ASSERT_EQ(f.adj[0].size(), (size_t)1);
}

Di GoogleTest asli, fixture adalah kelas turunan ::testing::Test dengan method SetUp() dan TearDown(). Setiap test fixture mendapat instance baru — state tidak bocor antar test.

Best Practices untuk DSA #

PraktikManfaat
Satu TEST() per behavior, bukan per fungsiTest menjadi dokumentasi hidup
Nama test: <Method>_<Scenario>_<Expectation>Misal BinarySearch_Found_ReturnsIndex
ASSERT_* untuk precondition, EXPECT_* untuk verifikasiHindari UB dari precondition gagal
Test nama unik agar mudah di-filter--gtest_filter=VectorTest.*
Hindari test yang saling bergantungParalelisasi aman, debug mudah

Ringkasan #

  • GoogleTest dan Catch2 adalah dua framework populer untuk unit testing di C++ modern.
  • EXPECT_* mencatat kegagalan tetapi melanjutkan; ASSERT_* menghentikan test seketika.
  • Macro TEST(Suite, Name) bekerja dengan static initializer yang mendaftarkan fungsi ke global registry.
  • Mini framework berbasis macro dapat mengimitasi pola GoogleTest tanpa dependensi eksternal — cukup untuk DSA dan prototyping cepat.
  • Fixture pattern (setup/teardown) menangani inisialisasi berulang — di mini framework dapat diadakan via struct dengan method setup().
  • Exit code non-zero bila ada failure adalah praktik wajib untuk integrasi CI/CD.