Bài 18 · Nâng cao · 26 phút· Cập nhật 11/06/2026
Integration test trong C
Biên soạn bởi Nguyễn Anh Tuấn
Kiểm các module GHÉP nhau ở chỗ tiếp giáp: round-trip lưu/đọc qua tệp, fixture, end-to-end CLI black-box, tháp kiểm thử.
Unit test bảo vệ từng module riêng. Nhưng phần mềm hỏng nhiều nhất ở chỗ tiếp giáp - nơi một module trao dữ liệu cho module khác. Module Store lưu ra tệp, module Storage đọc lại: mỗi cái RIÊNG có thể đúng, nhưng nếu định dạng lưu khác định dạng đọc, dữ liệu mất. Integration test kiểm đúng chỗ đó.
- ▸Unit = một module cô lập; Integration = nhiều module GHÉP ở chỗ tiếp giáp.
- ▸Loại bug đặc trưng: mỗi mảnh “đúng” nhưng hợp đồng GIỮA chúng lệch.
- ▸Công cụ chủ lực: round-trip (lưu → đọc lại → so khớp).
Thử cho store_save và store_load khớp rồi lệch định dạng - xem dữ liệu có “sống sót” qua tệp không:
1|1|hoc bai 2|0|di cho
- ▸Round-trip ĐẠT: lưu rồi đọc lại cho ra y nguyên - chỗ ghép khớp hợp đồng.
- ▸Round-trip HỎNG: saver dùng “|”, loader dùng “,” → mất hết; integration test bắt ngay.
- ▸Tháp: nhiều unit (đáy) · vừa integration · ít e2e (đỉnh) - mỗi tầng một loại lỗi.
Lưu bằng một store, rồi đọc lại bằng store MỚI - mô phỏng “tắt rồi mở lại app”. Để ý fixture: dùng tệp tạm (setup) và remove (teardown):
tests/test_roundtrip.c - integration test
#include "task.h"
#include "test.h"
#include <stdio.h>
static void test_save_load_roundtrip(void) {
const char *path = "test_roundtrip.tmp"; /* FIXTURE: tep tam */
/* ACT 1: dung store, them, danh dau, LUU ra tep */
TaskStore *a = store_create();
int id1 = store_add(a, "viec mot");
int id2 = store_add(a, "viec hai");
store_done(a, id1);
CHECK(store_save(a, path) == true);
store_destroy(a);
/* ACT 2: store MOI DOC LAI tu tep (nhu mo lai app) */
TaskStore *b = store_create();
CHECK(store_load(b, path) == true);
/* ASSERT: du lieu song sot qua bien gioi store <-> tep */
CHECK(store_count(b) == 2);
Task *t0 = store_at(b, 0);
Task *t1 = store_at(b, 1);
CHECK(t0 != NULL);
CHECK(t1 != NULL);
if (t0 && t1) { /* phong thu: load hong -> bao FAIL sach */
CHECK(t0->id == id1);
CHECK(t0->done == true); /* trang thai cung phai song sot */
CHECK(t1->done == false);
}
store_destroy(b);
remove(path); /* TEARDOWN: don tep tam */
} Vì sao bọc if (t0 && t1)
Tầng trên cùng: chạy todo như người dùng - cấp input qua stdin, kiểm output. Không biết ruột bên trong, chỉ “ngoài vào, ngoài ra”:
tests/run_cli.sh - e2e test
#!/bin/sh
set -eu
TODO="$1"
# FIXTURE: chay trong thu muc tam de todo.dat khong dinh nhau
work="$(mktemp -d)"; cd "$work"
out="$(printf 'them hoc bai\nthem di cho\nxong 1\nin\nthoat\n' | "$TODO")"
printf '%s\n' "$out"
echo "$out" | grep -q '\[x\] hoc bai' || { echo 'FAIL'; exit 1; }
echo "$out" | grep -q '\[ \] di cho' || { echo 'FAIL'; exit 1; }
echo 'e2e OK' CMakeLists.txt - đăng ký 2 loại test cho CTest
enable_testing()
# (1) integration: Store + Storage qua tep that
add_executable(test_roundtrip tests/test_roundtrip.c)
target_link_libraries(test_roundtrip PRIVATE todocore)
add_test(NAME roundtrip COMMAND test_roundtrip)
# (2) e2e: chay CLI that, kiem output
add_test(NAME cli_e2e
COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/tests/run_cli.sh $<TARGET_FILE:todo>) Kết quả khi chạy
Start 1: roundtrip
1/2 Test #1: roundtrip ...... Passed
Start 2: cli_e2e
2/2 Test #2: cli_e2e ....... Passed
100% tests passed, 0 tests failed out of 2Lab Store + Storage + CLI với 2 test: round-trip qua tệp & e2e CLI. Chạy ctest cho xanh, rồi đổi định dạng lưu để xem integration test bắt lỗi chỗ tiếp giáp.
$ cmake -S . -B build $ cmake --build build $ ctest --test-dir build --output-on-failure
Liên hệ Toolchain & tiếp theo
Câu hỏi thường gặp
Unit test kiểm MỘT module cô lập. Integration test kiểm NHIỀU module GHÉP với nhau ở chỗ tiếp giáp - vd Store lưu ra tệp rồi Storage đọc lại. Loại bug nó bắt: mỗi module riêng vẫn “đúng” nhưng hợp đồng GIỮA chúng lệch (định dạng lưu khác định dạng đọc).
Là test “khứ hồi”: đưa dữ liệu QUA một biên giới rồi LẤY LẠI, và khẳng định nó y nguyên. Ở đây: store_save ghi ra tệp, rồi một store MỚI store_load đọc về - mô phỏng tắt rồi mở lại app. Nếu lưu/đọc lệch nhau, dữ liệu không khớp và test đỏ.
Setup dựng môi trường cho test (tạo một tệp tạm), teardown dọn sạch sau đó (remove tệp). Nhờ vậy test không để lại rác và không phụ thuộc lần chạy trước - chạy lại bao nhiêu lần cũng cho cùng kết quả.
E2E chạy CHƯƠNG TRÌNH HOÀN CHỈNH như người dùng: cấp input qua stdin, kiểm output (black-box, không biết ruột). Integration thường gọi trực tiếp vài hàm/module. E2E bắt lỗi “ráp tất cả lại mới lộ”: thiếu liên kết, sai luồng tổng thể.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Integration test khác unit test ở điểm cốt lõi nào?
- 1
Round-trip xanh
Build lab và chạy
ctest. Hai test nào chạy, kết quả?Hoàn thành khi:
roundtrip+cli_e2e, cả hai pass: “100% tests passed ... 2”. - 2
Bắt lỗi chỗ tiếp giáp
Trong
store.c, đổistore_saveghi dấu “,” thay vì “|”. Chạy lại. Test nào đỏ và vì sao unit test khó bắt lỗi này?Hoàn thành khi:
roundtripđỏ (store_count != 2). Mỗi module riêng vẫn đúng; chỉ chỗ GHÉP (định dạng lưu≠đọc) mới lệch. - 3
Chỉ ra fixture
Trong
test_roundtrip.c, dòng nào là setup, dòng nào là teardown? Vì sao cần teardown?Hoàn thành khi: Setup: dùng path tệp tạm +
store_savetạo nó. Teardown:remove(path). Để không để lại rác / ảnh hưởng lần chạy sau. - 4
Round-trip cho count
Thêm một assert trong round-trip: sau khi load, tổng số việc CHƯA xong phải bằng lúc lưu.
Hoàn thành khi: Đếm
done==falseở cả hai bên vàCHECKbằng nhau; vẫn xanh khi định dạng khớp. - 5
Thêm e2e
Trong
run_cli.sh, thêm kịch bản: thêm việc rồi “xoa”, kiểm output KHÔNG còn việc đó.Hoàn thành khi: Script
grep -vđúng dòng;ctestcli_e2evẫn xanh khi CLI đúng, đỏ nếu xoa hỏng. - 6
Tháp của bạn
Với app Todo, kể 1 ví dụ test ở MỖI tầng (unit, integration, e2e).
Hoàn thành khi: Vd unit:
store_popLIFO; integration: save→load round-trip; e2e: chạy CLI thêm/in/thoát.