← Lập trình C nâng cao

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_savestore_load khớp rồi lệch định dạng - xem dữ liệu có “sống sót” qua tệp không:

Định dạng giữa store_savestore_load:
store (lưu)
#1 [x] hoc bai
#2 [ ] di cho
save →
todo.dat
1|1|hoc bai
2|0|di cho
load →
store mới (đọc)
#1 [x] hoc bai
#2 [ ] di cho
Round-trip ĐẠT - dữ liệu sống sót y nguyên qua biên giới store ↔ file. Integration test xanh.
Tháp kiểm thử - mỗi tầng bắt một loại lỗi:
E2E · Chậm nhất · ít nhất
Lỗi “ráp lại không chạy”: thiếu liên kết, sai luồng tổng thể.
Integration · Vừa
Lỗi ở CHỖ TIẾP GIÁP: lệch định dạng lưu/đọc, sai hợp đồng giữa module.
Unit · Nhanh nhất · nhiều nhất
Lỗi logic bên trong một đơn vị.
  • 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)

Nếu store_load hỏng, store rỗng và store_at trả NULL. Truy cập t0->id khi NULL sẽ segfault - test “sập” thay vì “báo đỏ”. Kiểm NULL trước rồi mới deref: test báo đỏ rõ ràng, đúng dòng.

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 2
Chạy trên máy bạn - kỹ năng thật cần gõ lệnh thật

Lab 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
Xem từng file (8) ↡

Liên hệ Toolchain & tiếp theo

Gắn cả unit · integration · e2e vào CI để chạy tự động mỗi push - học sâu ở khoá C Thực Chiến · Toolchain. Giờ bạn đã có đủ thiết kế (module, đóng gói) và kiểm thử (unit, integration) - bài cuối khoá sẽ ghép tất cả thành ba sản phẩm thật.

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ể.

Một tỉ lệ gợi ý: NHIỀU unit test (nhanh, rẻ, ở đáy), VỪA integration test, ÍT e2e test (chậm, đắt, ở đỉnh). Lý do: unit chạy hàng trăm cái mỗi giây bắt phần lớn lỗi; e2e ít nhưng cần để chắc “cả hệ thống chạy”.

Lập trình phòng thủ: nếu load HỎNG (trả về store rỗng), store_at trả NULL. Truy cập t0->id khi t0NULL sẽ segfault, làm test “sập” thay vì “báo thất bại rõ ràng”. Kiểm NULL trước rồi mới deref → test thất bại với thông báo rõ ràng.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Integration test khác unit test ở điểm cốt lõi nào?

  1. 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. 2

    Bắt lỗi chỗ tiếp giáp

    Trong store.c, đổi store_save ghi 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. 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_save tạo nó. Teardown: remove(path). Để không để lại rác / ảnh hưởng lần chạy sau.

  4. 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à CHECK bằng nhau; vẫn xanh khi định dạng khớp.

  5. 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; ctest cli_e2e vẫn xanh khi CLI đúng, đỏ nếu xoa hỏng.

  6. 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_pop LIFO; integration: save→load round-trip; e2e: chạy CLI thêm/in/thoát.