Chapter Unit Testing menutup dengan kesimpulan yang tidak selalu disangka: inti sebuah framework uji ternyata sederhana — jalankan assertion, catat yang gagal, laporkan ringkasan, kembalikan exit code. Mini harness macro CHECK buatan kita sendiri sudah membuktikannya. Chapter ini naik kelas ke framework yang dipakai proyek nyata: GoogleTest, standar de facto ekosistem C++ yang diandalkan proyek seperti Chrome, TensorFlow, dan LLVM. Alih-alih sekadar melihat sketsa, kita membangun dan menjalankan GoogleTest 1.14 sungguhan — keluarga assertion, fixture, matcher, filter binary uji, hingga integrasi CMake/CTest — lalu menutupnya dengan mini runner mandiri yang meniru mekanisme intinya.
Dari Mini Harness ke GoogleTest #
Framework penuh membawa fasilitas yang di harness mini harus kita tulis sendiri:
- keluarga assertion lengkap: pembandingan nilai, kondisi boolean, floating-point, hingga string;
- fixture: objek state yang disiapkan otomatis sebelum setiap test dan dirapikan sesudahnya;
- filter dan flag command line: memilih test yang dijalankan tanpa menyentuh kode;
- laporan kegagalan kaya: nilai kedua sisi perbandingan dicetak otomatis;
- integrasi build system: CTest dapat menemukan dan menjalankan test secara paralel.
Sandbox penulisan menyediakan GoogleTest 1.14 sebagai pustaka yang sudah dikompilasi sebelumnya: header ada di /tmp/googletest-1.14.0/googletest/include, implementasinya berupa satu object file /tmp/gtest-all.o, dan /tmp/gmock-all.o menambahkan lapisan matcher gmock dari proyek yang sama. Setiap program uji cukup dipasangkan dengan pola build berikut:
g++ -std=c++23 -I /tmp/googletest-1.14.0/googletest/include \
uji-pertama.cpp \
/tmp/gtest-all.o \
/tmp/googletest-1.14.0/googletest/src/gtest_main.cc \
-o uji-pertama -pthread
./uji-pertamaBendera -I menunjuk folder header agar #include <gtest/gtest.h> ditemukan compiler. Berkas gtest_main.cc menyediakan main() standar — dokumentasi resmi menyarankan menghubungkan program dengan gtest_main daripada menulis main() sendiri — dan -pthread diperlukan karena pustaka memakai thread untuk sebagian fiturnya.
Test Pertama: TEST dan EXPECT_EQ #
Macro TEST menerima dua nama: nama suite sebagai kelompok, dan nama test sebagai perilaku yang dibuktikan. Di dalam badannya, EXPECT_EQ(nilaiAktual, nilaiDiharapkan) mencatat kegagalan lengkap dengan nilai kedua sisi tanpa menghentikan eksekusi. Berikut tiga test untuk unit yang sama dengan chapter Unit Testing:
// uji-pertama.cpp — test pertama kita dengan GoogleTest.
// Dipasangkan dengan GoogleTest 1.14 melalui pola build pada chapter ini.
#include <gtest/gtest.h>
#include <string>
// ---- unit yang diuji (dipinjam dari chapter Unit Testing) ----
double konversiCelsiusKeFahrenheit(double celsius) {
return celsius * 9.0 / 5.0 + 32.0;
}
std::string singkat(const std::string& teks, std::size_t maksimum) {
if (teks.size() <= maksimum) {
return teks;
}
return teks.substr(0, maksimum - 3) + "...";
}
// ---- test: satu TEST untuk satu perilaku ----
TEST(KonversiSuhu, TitikBekuAir) {
EXPECT_EQ(konversiCelsiusKeFahrenheit(0.0), 32.0);
}
TEST(KonversiSuhu, TitikDidihAir) {
EXPECT_EQ(konversiCelsiusKeFahrenheit(100.0), 212.0);
}
TEST(Teks, SingkatMenyisakanEllipsis) {
EXPECT_EQ(singkat("helloworld", 7), std::string("hell..."));
EXPECT_TRUE(singkat("oke", 10) == "oke");
}Output yang Dihasilkan:
Running main() from /tmp/googletest-1.14.0/googletest/src/gtest_main.cc
[==========] Running 3 tests from 2 test suites.
[----------] Global test environment set-up.
[----------] 2 tests from KonversiSuhu
[ RUN ] KonversiSuhu.TitikBekuAir
[ OK ] KonversiSuhu.TitikBekuAir (0 ms)
[ RUN ] KonversiSuhu.TitikDidihAir
[ OK ] KonversiSuhu.TitikDidihAir (0 ms)
[----------] 2 tests from KonversiSuhu (0 ms total)
[----------] 1 test from Teks
[ RUN ] Teks.SingkatMenyisakanEllipsis
[ OK ] Teks.SingkatMenyisakanEllipsis (0 ms)
[----------] 1 test from Teks (0 ms total)
[----------] Global test environment tear-down
[==========] 3 tests from 2 test suites ran. (0 ms total)
[ PASSED ] 3 tests.Baris pertama — Running main() from ... — berasal dari main() standar yang kita pasangkan. Blok [==========] menandai judul eksekusi, pasangan [ RUN ]/[ OK ] menutup satu test beserta durasinya, dan [ PASSED ] merangkum semuanya per suite. Kehati-hatian dari referensi resmi: EXPECT_EQ pada dua C string (const char*) membandingkan alamat, bukan isi — untuk teks, pakai std::string atau EXPECT_STREQ.
EXPECT vs ASSERT: Melanjutkan atau Menghentikan #
Semua assertion GoogleTest terbagi dua keluarga: EXPECT_* menghasilkan kegagalan non-fatal (dicatat, eksekusi berlanjut), sedangkan ASSERT_* menghasilkan kegagalan fatal — di balik layar, assertion ini melakukan return dari function terkini sehingga sisa badan test dilewati. Unit di bawah sengaja bermasalah: tigaPertama seharusnya mengembalikan tiga elemen, tetapi salah membatasi hasil menjadi dua. Kedua test menemukan bug yang sama dengan assertion yang berbeda:
// uji-gagal.cpp — membedakan EXPECT (melanjutkan) dan ASSERT (menghentikan).
// Unit yang diuji sengaja bermasalah agar kedua jenis assertion gagal.
#include <gtest/gtest.h>
#include <vector>
// Unit yang diuji (berisi bug): seharusnya mengembalikan tiga elemen
// pertama, tetapi salah membatasi hasil menjadi dua elemen saja.
std::vector<int> tigaPertama(const std::vector<int>& sumber) {
return {sumber.begin(), sumber.begin() + 2};
}
TEST(Ekspektasi, EXPECTMelanjutkanEksekusi) {
std::vector<int> hasil = tigaPertama({10, 20, 30});
EXPECT_EQ(hasil.size(), 3u); // gagal, tetapi tidak fatal
EXPECT_EQ(hasil[0], 10); // tetap dieksekusi
EXPECT_EQ(hasil[1], 20); // tetap dieksekusi
}
TEST(Ekspektasi, ASSERTMenghentikanEksekusi) {
std::vector<int> hasil = tigaPertama({10, 20, 30});
ASSERT_EQ(hasil.size(), 3u); // gagal dan fatal: test dihentikan di sini
EXPECT_EQ(hasil[0], 10); // tidak pernah dieksekusi
EXPECT_EQ(hasil[1], 20); // tidak pernah dieksekusi
}Kompilasi dengan pola build yang sama seperti uji-pertama, lalu jalankan ./uji-gagal. Output yang Dihasilkan:
Running main() from /tmp/googletest-1.14.0/googletest/src/gtest_main.cc
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from Ekspektasi
[ RUN ] Ekspektasi.EXPECTMelanjutkanEksekusi
uji-gagal.cpp:15: Failure
Expected equality of these values:
hasil.size()
Which is: 2
3u
Which is: 3
[ FAILED ] Ekspektasi.EXPECTMelanjutkanEksekusi (0 ms)
[ RUN ] Ekspektasi.ASSERTMenghentikanEksekusi
uji-gagal.cpp:22: Failure
Expected equality of these values:
hasil.size()
Which is: 2
3u
Which is: 3
[ FAILED ] Ekspektasi.ASSERTMenghentikanEksekusi (0 ms)
[----------] 2 tests from Ekspektasi (0 ms total)
[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (0 ms total)
[ PASSED ] 0 tests.
[ FAILED ] 2 tests, listed below:
[ FAILED ] Ekspektasi.EXPECTMelanjutkanEksekusi
[ FAILED ] Ekspektasi.ASSERTMenghentikanEksekusi
2 FAILED TESTSDua test gagal di baris yang sama, tetapi perilakunya berbeda. Pada EXPECTMelanjutkanEksekusi, kegagalan di baris 15 tidak menghentikan apa pun: dua EXPECT berikutnya tetap dievaluasi dan lulus dalam senyap. Pada ASSERTMenghentikanEksekusi, kegagalan di baris 22 langsung mengakhiri function test sehingga dua assertion di bawahnya tidak pernah dieksekusi. Karena kegagalan yang terkumpul lebih informatif, dokumentasi resmi menyarankan EXPECT_* sebagai pilihan bawaan dan ASSERT_* hanya untuk kondisi yang membuat pelanjutan tidak bermakna — pointer null yang hendak dipakai, misalnya.
Fixture dengan TEST_F #
Bila beberapa test butuh state awal yang sama, duplikasi di setiap badan test cepat melelahkan. Solusinya fixture: class turunan ::testing::Test yang menampung objek bersama, dengan constructor atau SetUp() berjalan sebelum test dan TearDown() plus destructor sesudahnya. GoogleTest membuat objek fixture baru untuk setiap test, sehingga state tidak pernah terbawa antar test. Macro-nya TEST_F — F untuk fixture — dan argumen pertamanya nama class fixture:
// uji-fixture.cpp — TEST_F: state yang disiapkan segar untuk setiap test.
#include <gtest/gtest.h>
#include <map>
#include <string>
// ---- unit yang diuji: manajemen stok barang sederhana ----
class Stok {
public:
void tambah(const std::string& barang, int jumlah) {
data_[barang] += jumlah;
}
bool kurangi(const std::string& barang, int jumlah) {
auto it = data_.find(barang);
if (it == data_.end() || it->second < jumlah) {
return false;
}
it->second -= jumlah;
return true;
}
int sisa(const std::string& barang) const {
auto it = data_.find(barang);
return it == data_.end() ? 0 : it->second;
}
private:
std::map<std::string, int> data_;
};
// ---- fixture: turunan ::testing::Test yang menyiapkan state awal ----
class UjiStok : public ::testing::Test {
protected:
void SetUp() override { // berjalan sebelum SETIAP test
stok.tambah("Pulpen", 10);
stok.tambah("Buku", 5);
}
// void TearDown() override {} // opsional: berjalan sesudah setiap test
Stok stok; // constructor/destructor juga per test
};
TEST_F(UjiStok, SaldoAwalSesuaiSetUp) {
EXPECT_EQ(stok.sisa("Pulpen"), 10);
EXPECT_EQ(stok.sisa("Buku"), 5);
}
TEST_F(UjiStok, PenguranganBerhasil) {
EXPECT_TRUE(stok.kurangi("Pulpen", 4));
EXPECT_EQ(stok.sisa("Pulpen"), 6);
}
TEST_F(UjiStok, PenguranganDitolakBilaSaldoKurang) {
EXPECT_FALSE(stok.kurangi("Buku", 50));
EXPECT_EQ(stok.sisa("Buku"), 5); // saldo tidak berubah
}
TEST_F(UjiStok, PenguranganDitolakBilaBarangTakAda) {
EXPECT_FALSE(stok.kurangi("Penghapus", 1));
EXPECT_EQ(stok.sisa("Penghapus"), 0);
}Kompilasi seperti pola build di atas, lalu jalankan ./uji-fixture. Output yang Dihasilkan:
Running main() from /tmp/googletest-1.14.0/googletest/src/gtest_main.cc
[==========] Running 4 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 4 tests from UjiStok
[ RUN ] UjiStok.SaldoAwalSesuaiSetUp
[ OK ] UjiStok.SaldoAwalSesuaiSetUp (0 ms)
[ RUN ] UjiStok.PenguranganBerhasil
[ OK ] UjiStok.PenguranganBerhasil (0 ms)
[ RUN ] UjiStok.PenguranganDitolakBilaSaldoKurang
[ OK ] UjiStok.PenguranganDitolakBilaSaldoKurang (0 ms)
[ RUN ] UjiStok.PenguranganDitolakBilaBarangTakAda
[ OK ] UjiStok.PenguranganDitolakBilaBarangTakAda (0 ms)
[----------] 4 tests from UjiStok (0 ms total)
[----------] Global test environment tear-down
[==========] 4 tests from 1 test suite ran. (0 ms total)
[ PASSED ] 4 tests.Isolasi terlihat dari output: PenguranganDitolakBilaSaldoKurang masih melihat Buku berjumlah 5 meskipun test sebelumnya mengurangi Pulpen — setiap test menerima Stok segar dari constructor plus SetUp() miliknya. Untuk resource mahal yang aman dibagi antar test satu suite, GoogleTest menyediakan SetUpTestSuite() dan TearDownTestSuite() yang berjalan sekali per suite. Jebakan ejaan yang lazim: method-nya SetUp(), bukan Setup(); tulis override agar compiler memberi tahu bila salah.
Matcher: Ekspektasi yang Ekspresif #
EXPECT_EQ dan EXPECT_TRUE menutup sebagian besar kebutuhan, tetapi ekspektasi seperti “string ini memuat kata tertentu” lebih pas ditulis apa adanya. GoogleTest menyediakan EXPECT_THAT(nilai, matcher): matcher adalah objek yang tahu cara memeriksa nilai sekaligus menjelaskan kegagalan. Matcher — angka seperti Eq, Ne, Lt, string seperti HasSubstr — berasal dari lapisan gmock yang satu bundel dengan GoogleTest: sertakan header gmock/gmock.h dan pasangkan object /tmp/gmock-all.o pada pola build:
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <string>
using ::testing::Eq;
using ::testing::Ne;
using ::testing::Lt;
using ::testing::HasSubstr;
TEST(Matcher, PembandingDasar) {
int saldo = 2500;
EXPECT_THAT(saldo, Eq(2500));
EXPECT_THAT(saldo, Ne(0));
EXPECT_THAT(saldo, Lt(10000));
}
TEST(Matcher, EkspektasiString) {
std::string sapaan = "Halo, Budi!";
EXPECT_THAT(sapaan, HasSubstr("Budi"));
}g++ -std=c++23 -I /tmp/googletest-1.14.0/googletest/include \
-I /tmp/googletest-1.14.0/googlemock/include \
uji-matcher.cpp \
/tmp/gtest-all.o /tmp/gmock-all.o \
/tmp/googletest-1.14.0/googletest/src/gtest_main.cc \
-o uji-matcher -pthread
./uji-matcherOutput yang Dihasilkan:
Running main() from /tmp/googletest-1.14.0/googletest/src/gtest_main.cc
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from Matcher
[ RUN ] Matcher.PembandingDasar
[ OK ] Matcher.PembandingDasar (0 ms)
[ RUN ] Matcher.EkspektasiString
[ OK ] Matcher.EkspektasiString (0 ms)
[----------] 2 tests from Matcher (0 ms total)
[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (0 ms total)
[ PASSED ] 2 tests.Deklarasi using memindahkan nama matcher dari namespace testing agar badan test ringkas. Bila EXPECT_THAT gagal, matcher menulis sendiri penjelasannya — HasSubstr melaporkan substring yang dicari beserta isi string aktual. Matcher juga bisa digabung lewat komposisi AllOf/AnyOf; daftar lengkapnya ada pada dokumentasi.
Menjalankan dan Memfilter Binary Uji #
Binary uji membaca flag dari argv sebelum menjalankan test. Dua flag yang paling sering dipakai: --gtest_list_tests mencetak daftar test tanpa mengeksekusinya, dan --gtest_filter memilih subset dengan pola — * mewakili karakter apa pun, titik dua memisahkan beberapa pola:
./uji-fixture --gtest_filter='UjiStok.Pengurangan*'Output yang Dihasilkan:
Running main() from /tmp/googletest-1.14.0/googletest/src/gtest_main.cc
Note: Google Test filter = UjiStok.Pengurangan*
[==========] Running 3 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 3 tests from UjiStok
[ RUN ] UjiStok.PenguranganBerhasil
[ OK ] UjiStok.PenguranganBerhasil (0 ms)
[ RUN ] UjiStok.PenguranganDitolakBilaSaldoKurang
[ OK ] UjiStok.PenguranganDitolakBilaSaldoKurang (0 ms)
[ RUN ] UjiStok.PenguranganDitolakBilaBarangTakAda
[ OK ] UjiStok.PenguranganDitolakBilaBarangTakAda (0 ms)
[----------] 3 tests from UjiStok (0 ms total)
[----------] Global test environment tear-down
[==========] 3 tests from 1 test suite ran. (0 ms total)
[ PASSED ] 3 tests../uji-pertama --gtest_list_testsOutput yang Dihasilkan:
Running main() from /tmp/googletest-1.14.0/googletest/src/gtest_main.cc
KonversiSuhu.
TitikBekuAir
TitikDidihAir
Teks.
SingkatMenyisakanEllipsisFilter menerima nama lengkap Suite.Test maupun pola: UjiStok.Pengurangan* menjalankan tiga test sekaligus, sedangkan --gtest_list_tests menjadi cara cepat mengecek penamaan sebelum menyalinnya ke filter.
Mini Runner ala GoogleTest yang Kita Jalankan Sendiri #
Semua fasilitas di atas mendarat di mekanisme yang sama dengan harness mini chapter Unit Testing. Program berikut meniru tiga pilar inti GoogleTest: makro TEST yang mendaftarkan test ke registry sebelum main() berjalan, keluarga EXPECT_* yang mencatat kegagalan tanpa menghentikan test, dan main() yang menjalankan semuanya lalu mencetak ringkasan. Pendaftaran kini otomatis, dan program ini berdiri sendiri: cukup g++ biasa tanpa pustaka eksternal.
// uji-mini.cpp — mini test runner mandiri yang meniru mekanisme inti
// GoogleTest: makro TEST mendaftarkan test ke registry sebelum main()
// berjalan, EXPECT_* mencatat kegagalan tanpa menghentikan test, dan
// main() menjalankan semuanya lalu mencetak ringkasan. Tanpa dependensi
// eksternal: cukup dikompilasi dengan g++ biasa.
#include <cstdio>
#include <sstream>
#include <string>
#include <vector>
namespace mini {
// ---- registry test --------------------------------------------------
using FungsiUji = void (*)();
struct EntriUji {
const char* nama; // berformat "NamaSuite.NamaTest"
FungsiUji fungsi;
};
std::vector<EntriUji>& registry() {
static std::vector<EntriUji> daftar; // dibuat saat pertama dipakai
return daftar;
}
// Setiap makro TEST membuat satu Pendaftar; constructor-nya berjalan
// sebelum main() dan menitipkan test ke registry.
struct Pendaftar {
Pendaftar(const char* nama, FungsiUji fungsi) {
registry().push_back({nama, fungsi});
}
};
// ---- papan skor assertion --------------------------------------------
int gagalDiUjiAktif = 0; // kegagalan pada test yang sedang berjalan
int totalUjiGagal = 0; // banyaknya test yang berakhir gagal
void catatKegagalan(const char* berkas, int baris, const std::string& pesan) {
++gagalDiUjiAktif;
std::printf(" %s:%d: Gagal\n%s", berkas, baris, pesan.c_str());
}
template <typename T, typename U>
void cekSama(const T& aktual, const U& diharapkan, const char* ekspresi,
const char* berkas, int baris) {
if (aktual == diharapkan) {
return; // lulus: diam, mengikuti kebiasaan GoogleTest
}
std::ostringstream pesan;
pesan << " Ekspektasi: " << ekspresi
<< "\n Nilai aktual: " << aktual
<< "\n Nilai diharapkan: " << diharapkan << "\n";
catatKegagalan(berkas, baris, pesan.str());
}
template <typename T, typename U>
void cekBerbeda(const T& aktual, const U& diharapkan, const char* ekspresi,
const char* berkas, int baris) {
if (aktual != diharapkan) {
return;
}
std::ostringstream pesan;
pesan << " Ekspektasi: " << ekspresi
<< "\n Kedua nilai sama-sama: " << aktual << "\n";
catatKegagalan(berkas, baris, pesan.str());
}
void cekBenar(bool kondisi, const char* ekspresi,
const char* berkas, int baris) {
if (kondisi) {
return;
}
std::ostringstream pesan;
pesan << " Ekspektasi: " << ekspresi
<< "\n Kondisi bernilai false\n";
catatKegagalan(berkas, baris, pesan.str());
}
} // namespace mini
// ---- makro assertion ala GoogleTest -----------------------------------
// Operator # (stringification) menyimpan teks argumen sehingga laporan
// menampilkan ekspresi aslinya, bukan sekadar keterangan "gagal".
#define EXPECT_EQ(aktual, diharapkan) \
mini::cekSama((aktual), (diharapkan), \
#aktual " == " #diharapkan, __FILE__, __LINE__)
#define EXPECT_NE(aktual, diharapkan) \
mini::cekBerbeda((aktual), (diharapkan), \
#aktual " != " #diharapkan, __FILE__, __LINE__)
#define EXPECT_TRUE(kondisi) \
mini::cekBenar((kondisi), #kondisi, __FILE__, __LINE__)
// ---- makro TEST: mendefinisikan badan test DAN mendaftarkannya -------
#define TEST(namaSuite, namaTest) \
static void namaSuite##_##namaTest##_badan(); \
static mini::Pendaftar namaSuite##_##namaTest##_daftar( \
#namaSuite "." #namaTest, &namaSuite##_##namaTest##_badan); \
static void namaSuite##_##namaTest##_badan()
// ---- unit yang diuji: singkat versi diperbaiki -------------------------
// Perbaikan atas bug dari chapter Unit Testing: bila maksimum < 4,
// kembalikan potongan teks tanpa tanda titik tiga (pada maksimum == 3
// saja, tiga titik sudah memakai seluruh jatah karakter).
std::string singkat(const std::string& teks, std::size_t maksimum) {
if (teks.size() <= maksimum) {
return teks;
}
if (maksimum < 4) {
return teks.substr(0, maksimum);
}
return teks.substr(0, maksimum - 3) + "...";
}
int faktorial(int n) {
int hasil = 1;
for (int i = 2; i <= n; ++i) {
hasil *= i;
}
return hasil;
}
// ---- test yang terdaftar otomatis lewat makro TEST ----------------------
TEST(Teks, TeksPendekTidakDipotong) {
EXPECT_EQ(singkat("oke", 10), std::string("oke"));
EXPECT_NE(singkat("oke", 10), std::string("oke!"));
}
TEST(Teks, PemotonganStandarMenyisakanEllipsis) {
EXPECT_EQ(singkat("helloworld", 7), std::string("hell..."));
}
TEST(Teks, BatasKecilTanpaEllipsis) {
// mengunci perilaku tepi: maksimum 3 tanpa ruang untuk tanda titik
EXPECT_EQ(singkat("abcdef", 3), std::string("abc"));
EXPECT_TRUE(singkat("abcdef", 1) == "a");
}
TEST(Matematika, FaktorialNol) {
EXPECT_EQ(faktorial(0), 1);
}
TEST(Matematika, FaktorialLima) {
EXPECT_EQ(faktorial(5), 120);
}
// ---- main(): jalankan seluruh test yang terdaftar ------------------------
int main() {
const std::vector<mini::EntriUji>& daftar = mini::registry();
// Hitung test suite berbeda (bagian nama sebelum titik).
int jumlahSuite = 0;
std::string suiteSebelumnya;
for (const mini::EntriUji& e : daftar) {
std::string nama(e.nama);
std::string suite = nama.substr(0, nama.find('.'));
if (suite != suiteSebelumnya) {
++jumlahSuite;
suiteSebelumnya = suite;
}
}
std::printf("[==========] Menjalankan %d uji dari %d test suite.\n",
static_cast<int>(daftar.size()), jumlahSuite);
std::string suiteTercetak;
for (const mini::EntriUji& e : daftar) {
std::string nama(e.nama);
std::string suite = nama.substr(0, nama.find('.'));
if (suite != suiteTercetak) {
int dalamSuite = 0;
for (const mini::EntriUji& f : daftar) {
std::string n(f.nama);
if (n.substr(0, n.find('.')) == suite) {
++dalamSuite;
}
}
std::printf("[----------] %d uji dari %s\n", dalamSuite,
suite.c_str());
suiteTercetak = suite;
}
mini::gagalDiUjiAktif = 0;
std::printf("[ RUN ] %s\n", e.nama);
e.fungsi(); // assertion gagal dicatat, eksekusi tetap berlanjut
if (mini::gagalDiUjiAktif == 0) {
std::printf("[ OK ] %s\n", e.nama);
} else {
std::printf("[ GAGAL ] %s\n", e.nama);
++mini::totalUjiGagal;
}
}
std::printf("[==========] %d uji dari %d test suite selesai.\n",
static_cast<int>(daftar.size()), jumlahSuite);
if (mini::totalUjiGagal == 0) {
std::printf("[ PASSED ] %d uji.\n", static_cast<int>(daftar.size()));
return 0; // kontrak exit code: 0 berarti semua lulus
}
std::printf("[ GAGAL ] %d uji.\n", mini::totalUjiGagal);
return 1;
}Output yang Dihasilkan:
[==========] Menjalankan 5 uji dari 2 test suite.
[----------] 3 uji dari Teks
[ RUN ] Teks.TeksPendekTidakDipotong
[ OK ] Teks.TeksPendekTidakDipotong
[ RUN ] Teks.PemotonganStandarMenyisakanEllipsis
[ OK ] Teks.PemotonganStandarMenyisakanEllipsis
[ RUN ] Teks.BatasKecilTanpaEllipsis
[ OK ] Teks.BatasKecilTanpaEllipsis
[----------] 2 uji dari Matematika
[ RUN ] Matematika.FaktorialNol
[ OK ] Matematika.FaktorialNol
[ RUN ] Matematika.FaktorialLima
[ OK ] Matematika.FaktorialLima
[==========] 5 uji dari 2 test suite selesai.
[ PASSED ] 5 uji.Format [ RUN ], [ OK ], dan [ PASSED ] sengaja ditiru dari output GoogleTest di atas, termasuk kebiasaan diam pada assertion yang lulus — barisnya menjadi [ GAGAL ] hanya bila test berakhir gagal. Mari kita bedah mekanisme pendaftaran dan pelaporannya:
- Baris 15-33 (
registry(),EntriUji,struct Pendaftar): registry berupastd::vector<EntriUji>yang disimpan sebagai static lokal di dalam function — instance unik dibuat saat pertama dipakai, aman dari masalah urutan inisialisasi global.Pendaftarmenitipkan nama test dan pointer function ke registry lewat constructor-nya. - Baris 97-101 (makro
TEST): menghasilkan tiga deklarasi: badan test sebagai static function, satuPendaftarstatis yang mendaftar sebelummain(), lalu definisi badannya. Operator##menggabungkan suite dan test menjadi identifier unik; operator#(stringification) membentuk label sepertiTeks.BatasKecilTanpaEllipsis. - Baris 36-42 (papan skor):
gagalDiUjiAktifdireset per test (baris 185);totalUjiGagalmenghitung test yang gagal. Assertion hanya menaikkan penghitung — badan test tetap berlanjut, persis semantikEXPECT_*. - Baris 44-78 (
cekSama,cekBerbeda,cekBenar): template yang diam saat lulus dan merakit pesan lewatostringstreamsaat gagal;catatKegagalanmenambahkan lokasiberkas:barismengikuti gaya GoogleTest. - Baris 85-94 (makro
EXPECT_*):#aktual " == " #diharapkanmenyimpan teks ekspresi asli sehingga laporan menampilkansingkat("abcdef", 3) == std::string("abc");__FILE__dan__LINE__menambah lokasi tepat tanpa usaha penulis test. - Baris 103-115 (unit yang diuji):
singkatversi diperbaiki dari bug chapter Unit Testing — batas kinimaksimum < 4karena padamaksimum == 3tiga titik sudah memakai seluruh jatah karakter; keputusan perilaku ini dikunci oleh testBatasKecilTanpaEllipsis. - Baris 150-194 (loop
main()): menghitung suite dari bagian nama sebelum titik, lalu untuk setiap entri: reset penghitung, cetak[ RUN ], panggil function test, tutup dengan[ OK ]atau[ GAGAL ]. - Baris 198-204 (kontrak exit code): return 0 hanya bila
totalUjiGagal == 0— kontrak yang membuat mini runner ini, dan GoogleTest sungguhan, dipercaya pipeline CI.
Integrasi CMake dan CTest #
Di proyek nyata, GoogleTest hampir selalu masuk lewat CMake. Sandbox penulisan tidak memasang cmake maupun ctest, sehingga blok bagian ini adalah pola ilustratif dari dokumentasi resmi, bukan sesi yang kita jalankan. Jalur pertama dan paling terkendali: FetchContent mengunduh sumber GoogleTest saat konfigurasi (butuh Git):
cmake_minimum_required(VERSION 3.14) # CMakeLists.txt ilustrasi: butuh Git utk FetchContent
project(uji_cpp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # Windows: CRT sama dgn proyek induk
FetchContent_MakeAvailable(googletest)
enable_testing()
add_executable(uji uji-fixture.cpp)
target_link_libraries(uji PRIVATE GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(uji)Kunci pola di atas: GTest::gtest_main memberikan main() standar sehingga tidak ada main() ganda, dan gtest_discover_tests mendaftarkan setiap test sebagai test case CTest yang bisa dijalankan paralel. Jalur kedua: pustaka dari repositori distribusi:
sudo apt install libgtest-dev # Debian/Ubuntu: header + pustaka GoogleTest dari distrofind_package(GTest REQUIRED) # pakai pustaka GoogleTest yang terinstal di sistem
target_link_libraries(uji PRIVATE GTest::gtest_main)Versi pustaka apt mengikuti distribusi — bisa lebih tua atau lebih baru dari 1.14 — sedangkan FetchContent mengunci versi di CMakeLists.txt; untuk pipeline CI yang harus reprodusibel, FetchContent biasanya lebih aman. Pola sesinya seperti berikut:
$ cmake -S . -B build
-- The CXX compiler identification is GNU 14.2.0
-- Configuring done
-- Generating done
-- Build files have been written to: /home/budi/proyek-uji/build
$ cmake --build build
[ 25%] Building CXX object _deps/googletest-build/googletest/CMakeFiles/gtest.dir/src/gtest-all.cc.o
[ 75%] Building CXX object CMakeFiles/uji.dir/uji-fixture.cpp.o
[100%] Linking CXX executable uji
$ ctest --test-dir build --output-on-failure
Test project /home/budi/proyek-uji/build
Start 1: UjiStok.SaldoAwalSesuaiSetUp
1/4 Test #1: UjiStok.SaldoAwalSesuaiSetUp ........ Passed 0.01 sec
Start 2: UjiStok.PenguranganBerhasil
2/4 Test #2: UjiStok.PenguranganBerhasil ......... Passed 0.01 sec
Start 3: UjiStok.PenguranganDitolakBilaSaldoKurang
3/4 Test #3: UjiStok.PenguranganDitolakBilaSaldoKurang Passed 0.01 sec
Start 4: UjiStok.PenguranganDitolakBilaBarangTakAda
4/4 Test #4: UjiStok.PenguranganDitolakBilaBarangTakAda Passed 0.01 sec
100% tests passed, 0 tests failed out of 4
Total Test time (real) = 0.05 secctest melaporkan setiap test dengan status dan durasinya, dan exit code-nya mengikuti kontrak yang sama: 0 berarti semua lulus. Pola inilah yang dipanggil pipeline CI: commit memicu build, build memicu ctest, dan kegagalan test menghentikan perubahan sebelum digabungkan ke branch utama.
Perjalanan chapter ini membentuk lingkaran penuh: kita memakai GoogleTest sungguhan untuk menulis test, lalu membongkar mekanismenya dalam mini runner yang berdiri sendiri. Pola registry, pendaftaran mandiri, dan assertion yang mencatat tanpa menghentikan berlaku untuk membaca framework uji mana pun. Dari sini ada dua arah wajar: chapter Integration Testing pada section ini menguji kerja sama antar komponen dengan fixture yang menyentuh dunia nyata, dan dokumentasi GoogleTest menunggu dengan fitur lanjutan seperti death test, test terparameterisasi, dan mocking. Dengan GoogleTest menjadi kebiasaan, kegagalan kode tiba sebagai baris laporan yang bisa kita baca dan perbaiki — bukan kejutan dari pengguna.
Referensi: GoogleTest Primer , Assertions Reference , CMake FetchContent .