Bài 57.128 phút đọc
Kiểm thử đơn vị
Sau bài này bạn sẽ làm được
- Viết khung kiểm thử tối thiểu bằng macro
- Tổ chức bài kiểm thử theo module
- Chạy kiểm thử trong hệ thống dựng
- So sánh Unity, Check và Criterion
Khung kiểm thử cho C không cần phức tạp. Sáu mươi dòng macro là đủ cho phần lớn dự án, và bạn hiểu hết từng dòng. Bài này viết một cái, dùng nó, rồi so sánh với ba khung có sẵn để bạn biết khi nào nên chuyển.
#Vì sao tự viết khung
| Tự viết | Khung có sẵn | |
|---|---|---|
| Số dòng phải hiểu | 60 | Vài nghìn |
| Phụ thuộc thêm | Không | Một thư viện, và cách dựng nó |
| Chạy được ở đâu | Mọi nơi có trình biên dịch C | Nơi thư viện đó dựng được |
| Tự phát hiện bài kiểm thử | Không, bạn gọi tay | Có |
| Chạy từng bài riêng lẻ | Phải tự thêm | Có |
| Cách ly tiến trình khi sập | Không | Có, với Check và Criterion |
| Báo cáo XML cho CI | Phải tự thêm | Có |
#kt.h, sáu mươi dòng
kt.h
#ifndef KT_H
#define KT_H
#include <stdio.h>
#include <string.h>
#include <math.h>
extern int kt_tong, kt_hong;
extern const char *kt_nhom_hien;
#define KT_NHOM(ten) \
do { kt_nhom_hien = (ten); printf("\n-- %s\n", (ten)); } while (0)
#define KT_DUNG(dk) \
do { \
++kt_tong; \
if (dk) { printf(" ok %s\n", #dk); } \
else { ++kt_hong; \
printf(" HONG %s (%s:%d)\n", #dk, __FILE__, __LINE__); } \
} while (0)
#define KT_BANG_INT(a, b) \
do { \
++kt_tong; \
long long _a = (long long)(a), _b = (long long)(b); \
if (_a == _b) { printf(" ok %s == %s\n", #a, #b); } \
else { ++kt_hong; \
printf(" HONG %s == %s (%lld != %lld) (%s:%d)\n", \
#a, #b, _a, _b, __FILE__, __LINE__); } \
} while (0)
#define KT_BANG_CHUOI(a, b) \
do { \
++kt_tong; \
const char *_a = (a), *_b = (b); \
if (_a && _b && strcmp(_a, _b) == 0) { \
printf(" ok %s == %s\n", #a, #b); \
} else { ++kt_hong; \
printf(" HONG %s == %s (\"%s\" != \"%s\") (%s:%d)\n", \
#a, #b, _a ? _a : "(null)", _b ? _b : "(null)", \
__FILE__, __LINE__); } \
} while (0)
#define KT_GAN_BANG(a, b, sai_so) \
do { \
++kt_tong; \
double _a = (a), _b = (b); \
if (fabs(_a - _b) <= (sai_so)) { \
printf(" ok %s ~ %s\n", #a, #b); \
} else { ++kt_hong; \
printf(" HONG %s ~ %s (%g vs %g) (%s:%d)\n", \
#a, #b, _a, _b, __FILE__, __LINE__); } \
} while (0)
#define KT_TONG_KET() \
(printf("\n%d kiem tra, %d hong\n", kt_tong, kt_hong), \
kt_hong == 0 ? 0 : 1)
#define KT_DINH_NGHIA() \
int kt_tong = 0; int kt_hong = 0; const char *kt_nhom_hien = ""
#endif#Dùng nó
chuoi.c, mã cần kiểm thử
#include "chuoi.h"
#include <string.h>
#include <ctype.h>
size_t chuoi_cat_trang(char *s) {
if (s == NULL) return 0;
char *dau = s;
while (*dau && isspace((unsigned char)*dau)) ++dau;
size_t n = strlen(dau);
while (n > 0 && isspace((unsigned char)dau[n - 1])) --n;
memmove(s, dau, n);
s[n] = '\0';
return n;
}
int chuoi_bat_dau_bang(const char *s, const char *tien_to) {
if (s == NULL || tien_to == NULL) return 0;
size_t n = strlen(tien_to);
return strncmp(s, tien_to, n) == 0;
}
long chuoi_tim(const char *s, char c) {
if (s == NULL) return -1;
const char *p = strchr(s, c);
return p ? (long)(p - s) : -1;
}kt_chuoi.c
#include "kt.h"
#include "chuoi.h"
#include <string.h>
KT_DINH_NGHIA();
static void thu_cat_trang(void) {
KT_NHOM("chuoi_cat_trang");
char a[] = " xin chao ";
KT_BANG_INT(chuoi_cat_trang(a), 8);
KT_BANG_CHUOI(a, "xin chao");
char b[] = "khong co trang";
KT_BANG_INT(chuoi_cat_trang(b), 14);
char c[] = " "; /* toan trang */
KT_BANG_INT(chuoi_cat_trang(c), 0);
KT_BANG_CHUOI(c, "");
char d[] = ""; /* rong */
KT_BANG_INT(chuoi_cat_trang(d), 0);
KT_BANG_INT(chuoi_cat_trang(NULL), 0); /* NULL */
}
static void thu_bat_dau(void) {
KT_NHOM("chuoi_bat_dau_bang");
KT_DUNG(chuoi_bat_dau_bang("xin chao", "xin"));
KT_DUNG(chuoi_bat_dau_bang("xin chao", "")); /* tien to rong */
KT_DUNG(chuoi_bat_dau_bang("xin", "xin")); /* bang nhau */
KT_DUNG(!chuoi_bat_dau_bang("xin", "xin chao")); /* tien to dai hon */
KT_DUNG(!chuoi_bat_dau_bang("", "x"));
KT_DUNG(!chuoi_bat_dau_bang(NULL, "x"));
}
static void thu_tim(void) {
KT_NHOM("chuoi_tim");
KT_BANG_INT(chuoi_tim("abcabc", 'a'), 0); /* lan xuat hien dau */
KT_BANG_INT(chuoi_tim("abcabc", 'c'), 2);
KT_BANG_INT(chuoi_tim("abcabc", 'z'), -1);
KT_BANG_INT(chuoi_tim("", 'a'), -1);
KT_BANG_INT(chuoi_tim(NULL, 'a'), -1);
KT_BANG_INT(chuoi_tim("abc", '\0'), 3); /* CO Y: tim NUL */
}
int main(void) {
thu_cat_trang();
thu_bat_dau();
thu_tim();
return KT_TONG_KET();
}terminal
gcc -std=c11 -D__USE_MINGW_ANSI_STDIO=1 -Wall -Wextra -g kt_chuoi.c chuoi.c -o kt_chuoi.exe
./kt_chuoi.exe
-- chuoi_cat_trang
ok chuoi_cat_trang(a) == 8
ok a == "xin chao"
ok chuoi_cat_trang(b) == 14
ok chuoi_cat_trang(c) == 0
ok c == ""
ok chuoi_cat_trang(d) == 0
ok chuoi_cat_trang(NULL) == 0
-- chuoi_bat_dau_bang
ok chuoi_bat_dau_bang("xin chao", "xin")
ok chuoi_bat_dau_bang("xin chao", "")
ok chuoi_bat_dau_bang("xin", "xin")
ok !chuoi_bat_dau_bang("xin", "xin chao")
ok !chuoi_bat_dau_bang("", "x")
ok !chuoi_bat_dau_bang(NULL, "x")
-- chuoi_tim
ok chuoi_tim("abcabc", 'a') == 0
ok chuoi_tim("abcabc", 'c') == 2
ok chuoi_tim("abcabc", 'z') == -1
ok chuoi_tim("", 'a') == -1
ok chuoi_tim(NULL, 'a') == -1
ok chuoi_tim("abc", '\0') == 319 kiem tra, 0 hong
echo $?
0
#Tổ chức bài kiểm thử
Cấu trúc thư mục
du-an/
include/
chuoi.h
ds.h
src/
chuoi.c
ds.c
kiem-thu/
kt.h <- khung
kt_chuoi.c <- mot tep kiem thu cho MOI module
kt_ds.c
MakefileMakefile
CC := gcc
CFLAGS := -std=c11 -Wall -Wextra -Wpedantic -g -Iinclude
KT_SRC := $(wildcard kiem-thu/kt_*.c)
KT_BIN := $(KT_SRC:.c=.exe)
SRC := $(wildcard src/*.c)
.PHONY: kiem-thu sach
kiem-thu: $(KT_BIN)
@hong=0; \
for t in $(KT_BIN); do \
echo "=== $$t ==="; \
./$$t || hong=1; \
done; \
exit $$hong
kiem-thu/%.exe: kiem-thu/%.c $(SRC)
$(CC) $(CFLAGS) -Ikiem-thu $^ -o $@
sach:
rm -f $(KT_BIN)Kiểm thử phụ thuộc lẫn nhau
static Ds *g_ds; /* trang thai dung chung */
static void thu_tao(void) {
g_ds = ds_tao(4);
KT_DUNG(g_ds != NULL);
}
static void thu_them(void) {
KT_BANG_INT(ds_them(g_ds, 5), 0); /* can thu_tao chay truoc */
KT_BANG_INT(ds_so_luong(g_ds), 1); /* can khong ai them truoc do */
}
/* Doi thu tu goi trong main la ca bo hong.
Them mot bai kiem thu o giua cung the.
Va ban khong chay rieng duoc mot bai. */
Mỗi bài tự đủ
static void thu_them(void) {
KT_NHOM("ds_them");
Ds *d = ds_tao(4); /* tu chuan bi */
KT_DUNG(d != NULL);
if (d == NULL) return;
KT_BANG_INT(ds_them(d, 5), 0);
KT_BANG_INT(ds_so_luong(d), 1);
ds_huy(d); /* tu don dep */
}
/* Chay duoc rieng le, chay duoc theo moi thu tu,
va chay duoc nhieu lan lien tiep. */
#Ba khung có sẵn
| Unity | Check | Criterion | |
|---|---|---|---|
| Kích thước | Ba tệp, chép vào dự án | Thư viện, cần cài | Thư viện, cần cài |
| Nền tảng | Mọi nơi, kể cả vi điều khiển | POSIX chủ yếu | Linux, macOS, Windows |
| Tự phát hiện bài kiểm thử | Không, có script sinh mã | Có, qua macro | Có, qua thuộc tính liên kết |
| Cách ly tiến trình | Không | Có, mỗi bài một fork | Có |
| Báo cáo XML | Có, qua plugin | Có | Có, sẵn |
| Giả lập | CMock, cùng nhóm | Không có sẵn | Không có sẵn |
| Hợp nhất | Hệ nhúng | Dự án GNU, autotools | Dự án hiện đại |
Unity: nhỏ nhất, và của giới nhúng
#include "unity.h"
#include "chuoi.h"
void setUp(void) { } /* chay truoc MOI bai */
void tearDown(void) { } /* chay sau MOI bai */
void test_cat_trang_hai_dau(void) {
char a[] = " xin chao ";
TEST_ASSERT_EQUAL_INT(8, chuoi_cat_trang(a));
TEST_ASSERT_EQUAL_STRING("xin chao", a);
}
void test_cat_trang_rong(void) {
char a[] = "";
TEST_ASSERT_EQUAL_INT(0, chuoi_cat_trang(a));
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_cat_trang_hai_dau);
RUN_TEST(test_cat_trang_rong);
return UNITY_END();
}
/* Uu the that: KHONG can cai gi. Chep unity.c, unity.h,
unity_internals.h vao du an va dich cung.
Do la ly do no thong tri the gioi vi dieu khien,
noi ma "cai mot thu vien" thuong khong phai lua chon. */Check: cách ly tiến trình, đó là điểm mạnh thật
#include <check.h>
#include "chuoi.h"
START_TEST(cat_trang_hai_dau) {
char a[] = " xin chao ";
ck_assert_int_eq(chuoi_cat_trang(a), 8);
ck_assert_str_eq(a, "xin chao");
}
END_TEST
START_TEST(sap_thi_sao) {
char *p = NULL;
chuoi_cat_trang(p + 1); /* SAP */
}
END_TEST
Suite *chuoi_suite(void) {
Suite *s = suite_create("chuoi");
TCase *tc = tcase_create("co ban");
tcase_add_test(tc, cat_trang_hai_dau);
tcase_add_test(tc, sap_thi_sao);
tcase_add_test_raise_signal(tc, sap_thi_sao, SIGSEGV); /* MONG DOI sap */
suite_add_tcase(s, tc);
return s;
}
/* Check chay MOI bai trong mot tien trinh fork rieng.
Nghia la:
- mot bai sap khong giet ca bo
- bao cao chinh xac bai nao sap va bang tin hieu gi
- kiem thu duoc CA cac ham co y goi abort hoac exit
Voi khung tu viet, mot segfault lam ban mat toan bo ket qua
cua nhung bai chua chay. Do la ly do manh nhat de chuyen. */Criterion: tự phát hiện, không cần main
#include <criterion/criterion.h>
#include "chuoi.h"
Test(chuoi, cat_trang_hai_dau) {
char a[] = " xin chao ";
cr_assert_eq(chuoi_cat_trang(a), 8);
cr_assert_str_eq(a, "xin chao");
}
Test(chuoi, cat_trang_rong) {
char a[] = "";
cr_assert_eq(chuoi_cat_trang(a), 0);
}
/* KHONG co main. Criterion tu sinh.
Va no tu tim moi ham Test() bang cach dat chung vao mot
phan doan lien ket rieng, roi duyet phan doan do luc chay.
Chay rieng mot bai: */
./kt --filter 'chuoi/cat_trang_rong'
/* Bao cao cho CI: */
./kt --xml=ket-qua.xmlTự làm thử
- Chép
kt.hvào dự án của bạn và viết bài kiểm thử cho một module. - Cố ý làm một bài kiểm thử hỏng và xác nhận mã thoát khác không.
- Thêm macro
KT_KHAC_NULLvàKT_BANG_BO_NHOvào khung. - Viết mục tiêu
kiem-thutrong Makefile chạy hết mọi bộ rồi mới báo lỗi. - Tìm một bài kiểm thử trong dự án của bạn phụ thuộc vào bài khác và làm nó tự đủ.
- Viết một bài kiểm thử gây segfault và quan sát nó phá huỷ toàn bộ đầu ra của khung tự viết.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Sáu mươi dòng macro là đủ cho phần lớn dự án; chuyển sang khung có sẵn khi cần chạy riêng, cách ly tiến trình, hoặc báo cáo XML.
- Macro là bắt buộc vì
__FILE__và__LINE__phải lấy tại chỗ gọi; biến tạm để mỗi đối số chỉ được tính một lần. mainphải trả về mã thoát khác không khi có bài hỏng, nếu không CI luôn xanh.- Mỗi bài kiểm thử tự chuẩn bị và tự dọn dẹp, chạy được theo mọi thứ tự.
- Unity cho hệ nhúng, Check cho cách ly tiến trình, Criterion cho tự phát hiện và báo cáo.