Unit test pada chapter sebelumnya membuktikan bahwa setiap unit benar sendiri-sendiri — function konversiCelsiusKeFahrenheit mengembalikan nilai tepat, function singkat memotong teks sesuai kontrak. Pengalaman industri berkata lain soal bagian yang tersisa: sebagian besar bug nyata muncul di sambungan antar bagian, bukan di dalam satu bagian. Klasiknya: class yang menulis data memakai format id;isi, sementara class yang membacanya tiba-tiba berpikir pemisahnya adalah koma — kedua unit lulus unit test masing-masing, dan program tetap rusak saat dipasangkan. Integration testing menutup celah ini dengan menguji beberapa komponen bekerja sama, termasuk saat menyentuh dependensi nyata seperti berkas. Chapter ini membangun mini integration test yang benar-benar menulis dan membaca berkas di disk, lengkap dengan fixture-nya.
Unit vs Integration: Peta Pembagian #
Keduanya bukan pesaing — proyek sehat memakai keduanya dengan proporsi berbeda:
| Aspek | Unit test | Integration test |
|---|---|---|
| Objek yang diuji | satu function/class, terisolasi | beberapa komponen dipasangkan |
| Dependensi (berkas, jaringan) | dihindari atau diganti tiruan | boleh nyata, selama terkendali |
| Kecepatan | milidetik, ratusan test per eksekusi | lebih lambat, jumlahnya lebih sedikit |
| Yang paling sering ditemukan | logika salah di dalam unit | salah paham kontrak antar unit |
Perbandingan kasarnya: bila unit test berjumlah puluhan hingga ratusan, integration test biasanya puluhan — yang penting setiap jalur data utama antar komponen punya minimal satu test. Aturan praktis yang bagus untuk memutuskan jenis test: bila yang ingin Anda buktikan adalah “function ini menghitung dengan benar”, itu unit; bila bunyinya “data yang ditulis modul A bisa dibaca utuh oleh modul B”, itu integration.
Fixture: Menyiapkan dan Membersihkan Lingkungan Uji #
Integration test punya kebutuhan yang tidak dimiliki unit test murni: lingkungan. Berkas uji harus belum ada saat test pertama berjalan, ada di posisi yang benar saat test tengah, dan terhapus saat semua selesai — kalau tidak, test yang tadinya lulus bisa gagal di eksekusi kedua karena sisa eksekusi pertama. Pakaian untuk kebutuhan ini bernama fixture: kode setup yang dijalankan sebelum test dan teardown yang dijalankan sesudahnya, dijamin berjalan berpasangan meskipun test gagal di tengah jalan.
Framework penuh seperti GoogleTest mengotomatiskan ini dengan class fixture (TEST_F dan constructor/destructor-nya — lihat dokumentasinya lewat tautan di akhir chapter). Untuk versi mini kita, fixture cukup ditulis eksplisit: satu panggilan std::filesystem::remove sebelum test memastikan kondisi awal bersih, dan satu panggilan serupa sesudahnya merapikan tempat. Prinsipnya sama persis dengan versi framework — kepastian kondisi awal, kepastian pembersihan — hanya porsinya yang lebih kecil.
Menguji Alur Tulis-Baca Berkas Secara Utuh #
Program berikut memasangkan dua class yang belum pernah kita uji bersama: Penyimpan yang menulis catatan ke berkas, dan Pembaca yang membacanya kembali. Format kontraknya sederhana — satu catatan per baris, berbentuk id;isi:
// uji-integrasi.cpp — menguji dua modul bekerja sama: Penyimpan menulis
// catatan ke berkas, Pembaca membacanya kembali. Juga memakai fixture
// sederhana (setup-teardown) di sekitar berkas uji.
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
struct Catatan {
int id;
std::string isi;
};
class Penyimpan {
public:
explicit Penyimpan(std::string path) : path_(std::move(path)) {}
bool simpan(const std::vector<Catatan>& catatan) {
std::ofstream keluar(path_);
if (!keluar) {
return false;
}
for (const Catatan& c : catatan) {
keluar << c.id << ';' << c.isi << '\n';
}
return true;
}
private:
std::string path_;
};
class Pembaca {
public:
explicit Pembaca(std::string path) : path_(std::move(path)) {}
std::vector<Catatan> baca() {
std::vector<Catatan> hasil;
std::ifstream masuk(path_);
if (!masuk) {
return hasil;
}
std::string baris;
while (std::getline(masuk, baris)) {
const std::size_t posisi = baris.find(';');
if (posisi == std::string::npos) {
continue;
}
hasil.push_back({std::stoi(baris.substr(0, posisi)),
baris.substr(posisi + 1)});
}
return hasil;
}
private:
std::string path_;
};
static int jumlahLulus = 0;
static int jumlahGagal = 0;
void laporkan(bool ok, const std::string& deskripsi) {
if (ok) {
++jumlahLulus;
std::cout << "[ LULUS ] " << deskripsi << "\n";
} else {
++jumlahGagal;
std::cout << "[ GAGAL ] " << deskripsi << "\n";
}
}
bool vektorSama(const std::vector<Catatan>& a, const std::vector<Catatan>& b) {
if (a.size() != b.size()) {
return false;
}
for (std::size_t i = 0; i < a.size(); ++i) {
if (a[i].id != b[i].id || a[i].isi != b[i].isi) {
return false;
}
}
return true;
}
int main() {
const std::string path = "uji-integrasi-data.txt";
const std::vector<Catatan> sumber = {{1, "beli buku"},
{2, "kirim laporan"}};
// Fixture: setup — pastikan berkas uji belum ada.
std::filesystem::remove(path);
// Uji 1: membaca berkas yang belum ada mengembalikan hasil kosong.
std::vector<Catatan> awal = Pembaca(path).baca();
laporkan(awal.empty(), "baca berkas yang belum ada -> kosong");
// Uji 2: modul Penyimpan menulis tanpa galat.
bool tersimpan = Penyimpan(path).simpan(sumber);
laporkan(tersimpan, "Penyimpan::simpan -> true");
// Uji 3: modul Pembaca membaca kembali persis catatan yang ditulis.
std::vector<Catatan> kembali = Pembaca(path).baca();
laporkan(vektorSama(sumber, kembali), "round-trip Penyimpan -> Pembaca");
// Fixture: teardown — bersihkan berkas uji.
bool bersih = std::filesystem::remove(path);
std::cout << "teardown: berkas uji dihapus = " << (bersih ? "ya" : "tidak")
<< "\n";
std::cout << "ringkasan: " << jumlahLulus << " lulus, " << jumlahGagal
<< " gagal\n";
return jumlahGagal == 0 ? 0 : 1;
}Output yang Dihasilkan:
[ LULUS ] baca berkas yang belum ada -> kosong
[ LULUS ] Penyimpan::simpan -> true
[ LULUS ] round-trip Penyimpan -> Pembaca
teardown: berkas uji dihapus = ya
ringkasan: 3 lulus, 0 gagalBedah bagian-bagian yang menjadikannya integration test, bukan sekadar program biasa:
- Skenario uji 1 menguji kontrak tepi antar komponen dan dunia —
Pembacapada berkas yang belum ada harus mengembalikan vector kosong, bukan error. Keputusan seperti ini mudah berbeda antar penulis; karena tertulis di test, kontraknya sekarang terkunci. - Uji 3 adalah round-trip — data
sumberdikirim menembus dua class berbeda (simpanmenulis,bacamem-parsing) lalu dibandingkan elemen per elemen lewatvektorSama. BilaPenyimpanmenulis;tetapiPembacamencari,, test ini yang akan menyala — dan itulah kelas bug yang mustahil ditemukan unit test terpisah. - Fixture eksplisit — setup (
removesebelum uji 1) menjamin test tidak membaca sisa data lama; teardown (removesesudah uji 3) dilaporkan hasilnya (dihapus = ya) agar kegagalan pembersihan terlihat, bukan diam-diam. - Kontrak exit code yang sama — persis seperti mini harness di chapter sebelumnya,
mainmengembalikan 0 hanya bila semua lulus, sehingga test ini siap dijalankan oleh automasi tanpa mengubah apa pun.
Kebersihan Lingkungan Uji #
Versi mini ini sengaja memakai nama berkas tetap, uji-integrasi-data.txt, dan itu keputusan yang perlu Anda sadari: dua proses test yang berjalan bersamaan di direktori yang sama bisa saling menimpa berkas uji. Untuk test suite yang membesar, dua kebiasaan menjaga kebersihan ini: beri nama berkas uji yang unik per proses (misalnya dari std::filesystem::temp_directory_path() plus nama test), atau jalankan setiap test di direktori sementara sendiri. Keduanya memastikan integration test tetap deterministik — hasilnya sama di mesin mana pun dan di eksekusi ke berapa pun, syarat yang sama ketatnya dengan contoh kode pada seluruh course ini.
Integration Test di CI #
Integration test baru memberi nilai penuh bila berjalan otomatis setiap ada perubahan. Pola job CI-nya sederhana dan bergantung total pada kontrak exit code yang sudah kita bangun — langkah Run integration tests gagal otomatis bila program uji mengembalikan angka bukan nol:
jobs:
integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build dengan sanitizer
run: g++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined uji-integrasi.cpp -o uji-integrasi
- name: Run integration tests
run: ./uji-integrasiPerhatikan build-nya memakai sanitizer: integration test yang menyentuh berkas dan alokasi memori nyata adalah tempat yang tepat untuk memasang jaring AddressSanitizer — satu job, dua lapis pemeriksaan. Dengan unit test dan integration test berjalan di CI, proyek kita sudah punya bukti perilaku yang bisa dijalankan kapan saja; yang tersisa adalah menutup section ini dengan katalog kelas bug yang paling sering terjadi pada program C++, dan cara kita mencegahnya.
Referensi: cppreference — filesystem , GoogleTest Primer .