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 #
| Framework | Lisensi | Khas | Cocok untuk |
|---|---|---|---|
| GoogleTest (gtest) | BSD-3 | Sintaks TEST(), TEST_F, EXPECT_*, ASSERT_* | Proyek produksi C++ skala besar |
| Catch2 | BSL-1.0 | Single-header, TEST_CASE, REQUIRE, SECTION | Proyek kecil, header-only |
| doctest | MIT | Mirip Catch2 tetapi lebih cepat compile | Alternatif 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 #
#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:
[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 #
static std::vector<TestResult> g_results;: registry global yang menyimpan hasil semua test. Setiap test akan memiliki satu entri di sini.static bool g_failed;: flag yang di-settruebilaEXPECT_EQatauASSERT_EQgagal. Di-reset di akhir setiap test.#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.#define EXPECT_EQ(a, b): membandingkanadanbdenganoperator==. Bila tidak sama, setg_failed = truedan catat pesan keg_reason. Eksekusi test tetap berlanjut.#define ASSERT_EQ(a, b): sama denganEXPECT_EQ, tetapi bila gagal melakukanreturn;untuk keluar dari fungsi test seketika. Wajib untuk precondition.ASSERT_EQ(s.size(), (size_t)3);: diStackTest.PushPopOrder, kita pastikan stack berisi 3 elemen sebelumpop(). Bila tidak, akses kedata.back()akan undefined behavior.r.ok ? pass++ : fail++;: dimain, hitung jumlah pass/fail. Exit code1bila 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():
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 #
| Praktik | Manfaat |
|---|---|
Satu TEST() per behavior, bukan per fungsi | Test menjadi dokumentasi hidup |
Nama test: <Method>_<Scenario>_<Expectation> | Misal BinarySearch_Found_ReturnsIndex |
ASSERT_* untuk precondition, EXPECT_* untuk verifikasi | Hindari UB dari precondition gagal |
| Test nama unik agar mudah di-filter | --gtest_filter=VectorTest.* |
| Hindari test yang saling bergantung | Paralelisasi 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.