Bỏ qua điều hướng, tới nội dung chính
Học C
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ếtKhung có sẵn
Số dòng phải hiểu60Vài nghìn
Phụ thuộc thêmKhôngMột thư viện, và cách dựng nó
Chạy được ở đâuMọi nơi có trình biên dịch CNơi thư viện đó dựng được
Tự phát hiện bài kiểm thửKhông, bạn gọi tayCó
Chạy từng bài riêng lẻPhải tự thêmCó
Cách ly tiến trình khi sậpKhôngCó, với Check và Criterion
Báo cáo XML cho CIPhải tự thêmCó

#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') == 3
19 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
  Makefile
Makefile
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

UnityCheckCriterion
Kích thướcBa tệp, chép vào dự ánThư viện, cần càiThư viện, cần cài
Nền tảngMọi nơi, kể cả vi điều khiểnPOSIX chủ yếuLinux, macOS, Windows
Tự phát hiện bài kiểm thửKhông, có script sinh mãCó, qua macroCó, qua thuộc tính liên kết
Cách ly tiến trìnhKhôngCó, mỗi bài một forkCó
Báo cáo XMLCó, qua pluginCóCó, sẵn
Giả lậpCMock, cùng nhómKhông có sẵnKhông có sẵn
Hợp nhấtHệ nhúngDự án GNU, autotoolsDự á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.xml

Tự làm thử

  1. Chép kt.h vào dự án của bạn và viết bài kiểm thử cho một module.
  2. Cố ý làm một bài kiểm thử hỏng và xác nhận mã thoát khác không.
  3. Thêm macro KT_KHAC_NULL và KT_BANG_BO_NHO vào khung.
  4. Viết mục tiêu kiem-thu trong Makefile chạy hết mọi bộ rồi mới báo lỗi.
  5. 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ự đủ.
  6. 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.
  • main phả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.