Bỏ qua điều hướng, tới nội dung chính
Học C
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ộcVì sao khó kiểm thửNên giả lập
time, clock_gettimeKhông lặp lại được, chậmLuôn luôn
rand, randomKhông lặp lại đượcHoặc đặt hạt cố định
fopen, read, writeCần tệp thật, chậm, phụ thuộc hệThường
socket, recv, sendCần mạng, không ổn địnhLuôn luôn
mallocKhông ép được thất bạiKhi kiểm thử nhánh hết bộ nhớ
getenvPhụ thuộc môi trường chạyThườ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àmThay tệpLiên kết yếu--wrap
C chuẩnCóCóKhôngKhông
Đổi mã sản phẩmCó, đổi giao diệnKhôngThêm một thuộc tínhKhông
Thay lúc chạyCóKhôngKhôngKhông
Nhiều bản giả cùng lúcCóKhôngKhôngKhông
Gọi được bản thậtCó, tự lưuKhôngKhôngCó, qua __real_
Giả lập được thư viện chuẩnKhông trực tiếpKhôngKhôngCó
Đếm số lần gọiCóCó, nếu bản giả đếmCóCó
Chi phí lúc chạyMột lần gọi gián tiếpKhôngKhôngKhô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ử

  1. Tìm một hàm trong dự án của bạn gọi time và kiểm thử nó bằng con trỏ hàm.
  2. 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.
  3. Dùng --wrap=malloc để ép nhánh hết bộ nhớ chạy và kiểm tra không rò rỉ.
  4. 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.
  5. 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.
  6. Gom mọi phụ thuộc nền tảng của một module vào một struct NenTang duy 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ì.
  • --wrap là cách duy nhất giả lập malloc hay 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.