Bài 17 · Nâng cao · 26 phút· Cập nhật 11/06/2026
Unit test trong C
Biên soạn bởi Nguyễn Anh Tuấn
Kiểm một module C cô lập: Arrange · Act · Assert; phủ happy/edge/error; table-driven; framework tí hon → CTest → Unity.
Bạn vừa học cách dựng module (Stack, vec) và đóng gói thành thư viện. Câu hỏi tiếp theo của một senior: làm sao biết module chạy ĐÚNG - và vẫn đúng sau khi sửa? Câu trả lời là unit test: chương trình nhỏ gọi module với dữ liệu biết trước rồi khẳng định kết quả. Mỗi test theo khuôn Arrange · Act · Assert.
- ▸Unit = MỘT đơn vị (hàm/module) kiểm CÔ LẬP - nhanh, xác định, lặp được.
- ▸Arrange (dựng cảnh) → Act (gọi đúng một hành vi) → Assert (khẳng định kết quả).
- ▸Mỗi test kiểm một hành vi: khi đỏ, biết ngay hỏng ở đâu.
Giá trị lớn nhất của test là chống hồi quy (regression): bạn sửa code, bộ test chạy lại tức thì và báo đỏ đúng chỗ vừa làm hỏng. Thử “gài bug” vào module Stack và xem test nào bắt được:
- ▸Bộ test tốt KHOANH VÙNG lỗi: chỉ test chạm behavior hỏng mới đỏ.
- ▸Bug LIFO→FIFO làm đỏ các test phụ thuộc thứ tự; bug count làm đỏ các test đọc count.
- ▸Không có test, một sửa đổi nhỏ có thể âm thầm phá hành vi cũ.
Một framework test, thực chất, chỉ gồm ba thứ: cách viết khẳng định, cách gom & chạy, và một mã thoát để ctest đọc. Đây là cả framework trong ~20 dòng:
tests/test.h - framework tí hon
#include <stdio.h>
static int tests_checks = 0, tests_failed = 0;
/* 1) Mot khang dinh - KHONG abort de cac test sau van chay */
#define CHECK(cond) do { \
tests_checks++; \
if (!(cond)) { tests_failed++; \
printf(" THAT BAI: %s (dong %d)\n", #cond, __LINE__); } \
} while (0)
/* 2) Gom & chay mot ham test */
#define RUN(test) do { printf("== %s ==\n", #test); test(); } while (0)
/* 3) Bao cao + ma thoat cho CTest (0 = tat ca xanh) */
#define TEST_REPORT() \
(printf("\n%d kiem tra, %d that bai\n", tests_checks, tests_failed), \
tests_failed == 0 ? 0 : 1) #cond và __LINE__
Một bộ test tốt phủ happy path, biên, và đường lỗi. Để ý test pop khi rỗng - nó bảo vệ nửa hợp đồng mà happy path bỏ qua. Lab bài này dùng bản stack_pop(s, &out) trả 0/1 - khác bản pop trả thẳng giá trị ở bài Đóng gói thư viện - chính để báo được lỗi khi rỗng:
tests/test_stack.c - Arrange · Act · Assert
#include "stack.h"
#include "test.h"
/* error path: pop khi RONG phai duoc xu ly, khong duoc sap */
static void test_pop_khi_rong_duoc_xu_ly(void) {
Stack *s = stack_create(); /* ARRANGE */
int x = 123;
CHECK(stack_pop(s, &x) == 0); /* ACT + ASSERT: bao that bai */
CHECK(x == 123); /* va KHONG ghi vao *out */
stack_destroy(s);
}
/* table-driven: mot du lieu, lap qua nhieu truong hop */
static void test_table_driven(void) {
int input[] = {1, 2, 3, 4, 5};
int n = (int)(sizeof input / sizeof input[0]);
Stack *s = stack_create();
for (int i = 0; i < n; i++) CHECK(stack_push(s, input[i]) == 1);
for (int i = n - 1; i >= 0; i--) { /* pop ra thu tu nguoc */
int x = 0;
CHECK(stack_pop(s, &x) == 1);
CHECK(x == input[i]);
}
stack_destroy(s);
}
int main(void) {
RUN(test_pop_khi_rong_duoc_xu_ly);
RUN(test_table_driven);
/* ...cac test khac... */
return TEST_REPORT();
} Kết quả khi chạy
== test_pop_khi_rong_duoc_xu_ly == == test_table_driven == 17 kiem tra, 0 that bai
Đăng ký test với CMake để chạy bằng ctest - mã thoát khác 0 là test đỏ:
CMakeLists.txt (phần test)
add_library(stack src/stack.c)
target_include_directories(stack PUBLIC include)
enable_testing() # bat ctest
add_executable(test_stack tests/test_stack.c)
target_link_libraries(test_stack PRIVATE stack)
add_test(NAME stack_unit COMMAND test_stack) # ctest se chay no Dự án thật thường dùng framework có sẵn như Unity hay CMocka - thêm setUp/tearDown và nhiều kiểu assert. Cùng test LIFO, viết với Unity:
cùng test đó, bằng Unity
#include "unity.h"
#include "stack.h"
void setUp(void) {} /* chay TRUOC moi test */
void tearDown(void) {} /* chay SAU moi test */
void test_lifo(void) {
Stack *s = stack_create();
stack_push(s, 10);
stack_push(s, 20);
int x = 0;
stack_pop(s, &x);
TEST_ASSERT_EQUAL_INT(20, x); /* assert co kieu cua Unity */
stack_destroy(s);
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_lifo);
return UNITY_END();
} Lab module Stack + framework tí hon + CTest. Chạy ctest cho xanh, gài bug để xem test bắt, rồi thêm test mới. Chạy thử dưới sanitizer với -DSAN=ON.
$ cmake -S . -B build $ cmake --build build $ ctest --test-dir build --output-on-failure
Liên hệ Toolchain
Tiếp theo: integration test
Câu hỏi thường gặp
Unit test kiểm MỘT đơn vị (một hàm/module) CÔ LẬP - nhanh, không đụng file/mạng. Integration test kiểm NHIỀU đơn vị GHÉP với nhau ở chỗ tiếp giáp (vd module + lưu file). Bài này lo unit; integration ở bài tiếp theo.
Khuôn ba bước của một test: ARRANGE dựng cảnh (tạo stack, đẩy vài phần tử); ACT gọi đúng MỘT hành vi cần kiểm (stack_pop); ASSERT khẳng định kết quả mong đợi (giá trị + mã trả về). Một test tốt chỉ kiểm một hành vi để khi đỏ, bạn biết ngay hỏng ở đâu.
Vì bug hay nấp ở biên và đường lỗi: pop khi rỗng, malloc thất bại, chỉ số ngoài phạm vi. Hợp đồng của hàm gồm CẢ các trường hợp này (vd “pop rỗng trả 0, không chạm *out”) - nên phải có test cho chúng, nếu không nửa hợp đồng không được bảo vệ.
Là một bản GIẢ thay cho một phụ thuộc thật, để ép một tình huống khó tái hiện. Vd muốn test nhánh stack_create trả NULL khi hết bộ nhớ, bạn thay malloc bằng một bản luôn thất bại (một “stub”). Nhờ đó test được đường lỗi mà bình thường gần như không xảy ra.
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.
Khuôn Arrange · Act · Assert của một unit test nghĩa là gì?
- 1
Đọc hợp đồng
Mở
stack.htrong lab.stack_pophứa gì khi stack RỖNG? Viết bằng lời.Hoàn thành khi: Trả về 0 (thất bại) và KHÔNG ghi vào
*out. Đó là hành vi mà test đường lỗi phải kiểm. - 2
Chạy bộ test
Build lab và chạy
ctest. Kết quả?Hoàn thành khi:
ctestxanh: “17 kiem tra, 0 that bai”, 1/1 test pass. - 3
Để test bắt bug
Sửa
stack_poplấys->data[0](biến LIFO thành FIFO), chạy lại. Test nào đỏ?Hoàn thành khi:
test_push_roi_pop_la_lifobáo “THAT BAI: x == 20”; table-driven cũng đỏ. Khôi phục để xanh lại. - 4
Thêm một unit test
Viết
test_push_lon_hon_suc_chua: push 6 phần tử (ép mảng grow), kiểmcount == 6và pop ra đủ.Hoàn thành khi: Test mới xanh; nếu grow sai, nó bắt được.
- 5
Nghĩ về test double
Muốn test nhánh
stack_createtrảNULL(hết bộ nhớ) mà không thật sự hết RAM - làm sao?Hoàn thành khi: Thay
mallocbằng một stub luôn trảNULL(test double), rồi khẳng địnhstack_createtrảNULL. - 6
Test dưới sanitizer
Build lab với
-DSAN=ONrồi chạyctest. Vì sao nên chạy test dưới ASan?Hoàn thành khi: Test vừa kiểm KẾT QUẢ đúng, vừa lộ lỗi bộ nhớ ẩn (rò rỉ, ngoài biên) khi chạy.