Bài 57.328 phút đọc
Giả lập phụ thuộc
Sau bài này bạn sẽ làm được
- Tiêm phụ thuộc qua con trỏ hàm
- Dùng liên kết yếu để thay hàm lúc liên kết
- Dùng --wrap của trình liên kết
- Chọn cách phù hợp với từng loại phụ thuộc
Hàm của bạn gọi time, rand, fopen, hoặc recv. Bạn không kiểm thử được nó, vì kết quả phụ thuộc vào đồng hồ, may rủi, đĩa, hay mạng. C có bốn cách thay những thứ đó bằng bản giả, và cả bốn đều chạy trong bài này.
#Vấn đề
phien.c, không kiểm thử được
#include "phien.h"
#include "dong_ho.h"
void phien_mo(Phien *p, int het_han_sau) {
p->tao_luc = lay_gio();
p->het_han_sau = het_han_sau;
}
int phien_con_song(const Phien *p) {
return lay_gio() - p->tao_luc < p->het_han_sau;
}
/* dong_ho.c */
time_t lay_gio(void) { return time(NULL); }Ranh giới nền tảng
Tập hợp các hàm mà mã của bạn gọi ra bên ngoài: đồng hồ, số ngẫu nhiên, hệ thống tệp, mạng, cấp phát bộ nhớ, biến môi trường. Đó là những chỗ duy nhất cần giả lập. Logic thuần tuý không cần.
| Phụ thuộc | Vì sao khó kiểm thử | Nên giả lập |
|---|---|---|
| time, clock_gettime | Không lặp lại được, chậm | Luôn luôn |
| rand, random | Không lặp lại được | Hoặc đặt hạt cố định |
| fopen, read, write | Cần tệp thật, chậm, phụ thuộc hệ | Thường |
| socket, recv, send | Cần mạng, không ổn định | Luôn luôn |
| malloc | Không ép được thất bại | Khi kiểm thử nhánh hết bộ nhớ |
| getenv | Phụ thuộc môi trường chạy | Thường |
| Hàm tính toán thuần tuý | Không khó | KHÔNG bao giờ |
#Một: con trỏ hàm
phien2.h, phụ thuộc nằm trong cấu trúc
#include <time.h>
typedef time_t (*HamGio)(void *ctx);
typedef struct {
HamGio gio; /* phu thuoc duoc TIEM VAO */
void *gio_ctx;
time_t tao_luc;
int het_han_sau;
} Phien2;
void phien2_khoi_tao(Phien2 *p, HamGio gio, void *ctx);
void phien2_mo(Phien2 *p, int het_han_sau);
int phien2_con_song(const Phien2 *p);
time_t gio_that(void *ctx);phien2.c
time_t gio_that(void *ctx) { (void)ctx; return time(NULL); }
void phien2_khoi_tao(Phien2 *p, HamGio gio, void *ctx) {
p->gio = gio ? gio : gio_that; /* NULL -> mac dinh that */
p->gio_ctx = ctx;
p->tao_luc = 0;
p->het_han_sau = 0;
}
void phien2_mo(Phien2 *p, int het_han_sau) {
p->tao_luc = p->gio(p->gio_ctx);
p->het_han_sau = het_han_sau;
}
int phien2_con_song(const Phien2 *p) {
return p->gio(p->gio_ctx) - p->tao_luc < p->het_han_sau;
}kt_tiem.c
/* Dong ho gia: doc tu mot bien do bai kiem thu dieu khien */
static time_t gio_gia(void *ctx) { return *(time_t *)ctx; }
/* Dong ho gia thu hai: dem so lan duoc goi */
static int so_lan = 0;
static time_t gio_dem(void *ctx) { ++so_lan; return *(time_t *)ctx; }
int main(void) {
KT_NHOM("phien2 voi dong ho tiem qua con tro ham");
time_t t = 100;
Phien2 p;
phien2_khoi_tao(&p, gio_gia, &t);
phien2_mo(&p, 5);
KT_BANG_INT(p.tao_luc, 100);
t = 104; KT_DUNG(phien2_con_song(&p)); /* con 1 giay */
t = 105; KT_DUNG(!phien2_con_song(&p)); /* dung diem het han */
KT_NHOM("dem so lan goi phu thuoc");
so_lan = 0;
Phien2 q;
phien2_khoi_tao(&q, gio_dem, &t);
phien2_mo(&q, 5);
phien2_con_song(&q);
phien2_con_song(&q);
KT_BANG_INT(so_lan, 3); /* 1 mo + 2 kiem tra */
KT_NHOM("NULL thi dung dong ho that");
Phien2 r;
phien2_khoi_tao(&r, NULL, NULL);
KT_DUNG(r.gio == gio_that);
return KT_TONG_KET();
}terminal
gcc -std=c11 -D__USE_MINGW_ANSI_STDIO=1 -Wall -Wextra -g kt_tiem.c phien2.c -o kt_tiem.exe
./kt_tiem.exe
-- phien2 voi dong ho tiem qua con tro ham ok p.tao_luc == 100 ok phien2_con_song(&p) ok !phien2_con_song(&p) -- dem so lan goi phu thuoc ok so_lan == 3 -- NULL thi dung dong ho that ok r.gio == gio_that 5 kiem tra, 0 hong
#Hai: thay tệp khi liên kết
dong_ho_gia.c, bản thật và bản giả có cùng ký hiệu
/* dong_ho.c, ban that, dung khi dung san pham */
#include "dong_ho.h"
time_t lay_gio(void) { return time(NULL); }
/* dong_ho_gia.c, ban gia, dung khi dung bo kiem thu */
#include "dong_ho.h"
time_t gio_gia_tri = 3000;
time_t lay_gio(void) { return gio_gia_tri; }kt_thay.c
#include "kt.h"
#include "phien.h"
KT_DINH_NGHIA();
extern time_t gio_gia_tri; /* dieu khien ban gia tu day */
int main(void) {
KT_NHOM("phien voi dong ho gia (thay tep .c)");
Phien p;
gio_gia_tri = 3000;
phien_mo(&p, 10);
KT_BANG_INT(p.tao_luc, 3000);
KT_DUNG(phien_con_song(&p));
gio_gia_tri = 3010;
KT_DUNG(!phien_con_song(&p));
return KT_TONG_KET();
}terminal
# San pham: lien ket dong_ho.c
gcc -std=c11 chinh.c phien.c dong_ho.c -o ct.exe
# Kiem thu: lien ket dong_ho_gia.c THAY VAO
gcc -std=c11 -D__USE_MINGW_ANSI_STDIO=1 -Wall -Wextra -g kt_thay.c phien.c dong_ho_gia.c -o kt_thay.exe
./kt_thay.exe
-- phien voi dong ho gia (thay tep .c) ok p.tao_luc == 3000 ok phien_con_song(&p) ok !phien_con_song(&p) 3 kiem tra, 0 hong
#Ba: liên kết yếu
dong_ho_yeu.c
#include "dong_ho.h"
/* weak: neu co dinh nghia MANH o noi khac, ban do thang. */
__attribute__((weak)) time_t lay_gio(void) { return time(NULL); }kt_yeu.c
#include "kt.h"
#include "phien.h"
#include "dong_ho.h"
KT_DINH_NGHIA();
static time_t gio_gia = 1000;
/* Dinh nghia MANH, de len ban yeu trong dong_ho_yeu.c */
time_t lay_gio(void) { return gio_gia; }
int main(void) {
KT_NHOM("phien voi dong ho gia (lien ket yeu)");
Phien p;
gio_gia = 2000;
phien_mo(&p, 30);
KT_BANG_INT(p.tao_luc, 2000);
KT_DUNG(phien_con_song(&p));
gio_gia = 2030;
KT_DUNG(!phien_con_song(&p));
return KT_TONG_KET();
}terminal
# CA HAI tep deu duoc lien ket, khong co xung dot
gcc -std=c11 -D__USE_MINGW_ANSI_STDIO=1 -Wall -Wextra -g kt_yeu.c phien.c dong_ho_yeu.c -o kt_yeu.exe
./kt_yeu.exe
-- phien voi dong ho gia (lien ket yeu) ok p.tao_luc == 2000 ok phien_con_song(&p) ok !phien_con_song(&p) 3 kiem tra, 0 hong
#Bốn: --wrap
kt_wrap.c
#include "kt.h"
#include "phien.h"
#include <stdio.h>
KT_DINH_NGHIA();
/* Ban gia cua lay_gio, do ta dieu khien */
static time_t gio_gia = 1000;
time_t __wrap_lay_gio(void) { return gio_gia; }
int main(void) {
KT_NHOM("phien voi dong ho gia (--wrap)");
Phien p;
gio_gia = 1000;
phien_mo(&p, 60);
KT_BANG_INT(p.tao_luc, 1000);
KT_DUNG(phien_con_song(&p));
gio_gia = 1059; /* con 1 giay */
KT_DUNG(phien_con_song(&p));
gio_gia = 1060; /* dung luc het han */
KT_DUNG(!phien_con_song(&p));
gio_gia = 5000; /* qua lau */
KT_DUNG(!phien_con_song(&p));
return KT_TONG_KET();
}terminal
# dong_ho.c KHONG doi gi, van la ban that
gcc -std=c11 -D__USE_MINGW_ANSI_STDIO=1 -Wall -Wextra -g kt_wrap.c phien.c dong_ho.c -Wl,--wrap=lay_gio -o kt_wrap.exe
./kt_wrap.exe
-- phien voi dong ho gia (--wrap) ok p.tao_luc == 1000 ok phien_con_song(&p) ok phien_con_song(&p) ok !phien_con_song(&p) ok !phien_con_song(&p) 5 kiem tra, 0 hong
#Chọn cách nào
| Con trỏ hàm | Thay tệp | Liên kết yếu | --wrap | |
|---|---|---|---|---|
| C chuẩn | Có | Có | Không | Không |
| Đổi mã sản phẩm | Có, đổi giao diện | Không | Thêm một thuộc tính | Không |
| Thay lúc chạy | Có | Không | Không | Không |
| Nhiều bản giả cùng lúc | Có | Không | Không | Không |
| Gọi được bản thật | Có, tự lưu | Không | Không | Có, qua __real_ |
| Giả lập được thư viện chuẩn | Không trực tiếp | Không | Không | Có |
| Đếm số lần gọi | Có | Có, nếu bản giả đếm | Có | Có |
| Chi phí lúc chạy | Một lần gọi gián tiếp | Không | Không | Không |
Giả lập quá nhiều
/* Gia lap ca cac ham noi bo cua chinh minh */
Test(don_hang, tinh_tong) {
gia_lap(tinh_thue, tra_ve(10));
gia_lap(tinh_giam_gia, tra_ve(5));
gia_lap(lam_tron, tra_ve(100));
KT_BANG_INT(don_hang_tong(&d), 105);
}
/* Bai kiem thu nay chi kiem tra mot phep cong.
Neu tinh_thue sai, no van xanh.
Neu ban doi cach don_hang_tong goi cac ham, no do
du ket qua van dung.
Day la kiem thu HIEN THUC, khong phai kiem thu HANH VI.
No lam moi lan tai cau truc deu pha vo bo kiem thu. */
Giả lập ở ranh giới
/* Chi gia lap ranh gioi nen tang */
Test(don_hang, tinh_tong) {
NenTang nt = { .gio = gio_gia, .ctx = &t };
DonHang d;
don_hang_khoi_tao(&d, &nt);
don_hang_them(&d, "sach", 100000, 2);
don_hang_them(&d, "but", 15000, 3);
/* Goi ma THAT: tinh_thue, tinh_giam_gia, lam_tron */
KT_BANG_INT(don_hang_tong(&d), 269500);
}
/* Kiem thu HANH VI: dua vao gi, ra gi.
Tai cau truc ben trong thoai mai, bai kiem thu van xanh.
Va no thuc su kiem chung logic tinh thue. */
Tự làm thử
- Tìm một hàm trong dự án của bạn gọi
timevà kiểm thử nó bằng con trỏ hàm. - Cài cùng một bản giả bằng cả bốn cách và so sánh số dòng phải sửa.
- Dùng
--wrap=mallocđể ép nhánh hết bộ nhớ chạy và kiểm tra không rò rỉ. - Viết bản giả đếm số lần gọi và kiểm chứng một hàm không gọi thừa.
- Thử liên kết yếu, cố ý gõ sai tên bản giả, và quan sát bài kiểm thử hỏng như thế nào.
- Gom mọi phụ thuộc nền tảng của một module vào một struct
NenTangduy nhấ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
- Chỉ giả lập ranh giới nền tảng: đồng hồ, ngẫu nhiên, tệp, mạng, cấp phát. Không giả lập logic thuần tuý.
- Con trỏ hàm là C chuẩn, thay được lúc chạy, nhiều bản cùng lúc; đổi lại là đổi giao diện.
- Thay tệp khi liên kết là cách đơn giản nhất, nhưng tất cả hoặc không có gì.
--wraplà cách duy nhất giả lậpmallochay hàm thư viện chuẩn, và bản giả gọi được bản thật qua__real_.- Giả lập quá nhiều biến kiểm thử hành vi thành kiểm thử hiện thực, và bộ kiểm thử vỡ mỗi lần tái cấu trúc.