使用 fff.h 來為 Google Test 製作各種 Test Double

前一篇介紹了如何在 Windows 上使用 Google Test 來 Unittest 既有的 Legacy C 語言程式碼,這一篇來教大家如何引入 fff 來加入不同類型的 Test Double(測試用假物件)。

Test Doubles

想要真正做到「單元」測試,就必須為其他人的程式碼作假,以避免其他人的程式碼干擾你想測試的部分。而這些暫時的替代品,就稱為 Test Double。其中又可分為 4 種用途:

  1. Dummy:只是用來 Build 過。
  2. Fake:可行的簡單實作。
  3. Stub:用來安插指定 Function 的返回值,以測試不同情況下的 Flow。
  4. Mock:用來檢查是否有正確使用外部 Function。

底下用一個簡單的例子説明:

extern "C" {
// here are the magic parts, will be explained in the following sections
#include "fff.h"
DEFINE_FFF_GLOBALS;
FAKE_VALUE_FUNC(int, get_hw_status);
FAKE_VOID_FUNC_VARARG(add_log, const unsigned char *, ...);
}

void some_hw_mux() {}; // dummy

int very_important_func()
{
    int hw_status;

    some_hw_mux();

    hw_status = get_hw_status();
    add_log("HW_STS %d", hw_status);

    return (hw_status == 2) ? TRUE: FALSE;
}

TEST(TestFunc, Case1) {
    int rtn;
    get_hw_status_fake.return_val = 2; // stub
    rtn = very_important_func();
    ASSERT_EQ(rtn, TRUE);
    ASSERT_EQ(add_log_fake.call_count, 1); // mock
}

其中,可以發現測試對象 very_important_func 的執行結果依賴 get_hw_status,因此需要為它實現 Stub,以提前指定好返回值,來完成後面的 ASSERT_EQ 檢查。又我們想要檢查 very_important_func 有確實呼叫 add_log,所以必須實現 Mock。另外雖然會呼叫到 some_hw_mux,但因為它不影響整體行為,只需要為其加入 Dummy 即可。

雖然 Test Doubles 分為 4 種用途,但事務上並不需要強記各自差別,當你遇到想要繞掉的 Function 的時候,自然就會使用到相對應的假物件。

後面文章內若出現 Mock 字眼,一律泛指:製作某不特定 Test Double。

fff (Fake Function Framework)

雖然 Google Test 有內建 GMock,但那主要是針對 C++ 的 Class 做 Test Doubles,並不適合用於 C function。因此本篇選擇導入 fff (Fake Function Framework),只需要簡單引入一支 Header 檔就能開始使用,為我們的 C Function 實現各種 Test Doubles。

因為是要 Mock 掉 C Function,所以在 Test 的 .cc 檔案中一樣要寫在 extern "C" {} block 裡面。加入這幾行:

extern "C" {
#include "fff.h"
DEFINE_FFF_GLOBALS;
// ... start your fake functions maker
}

Use Cases

如果想要 Mock 掉如下的宣告:

void some_hw_mux(void);

可以使用:

FAKE_VOID_FUNC(some_hw_mux);

如果後面有任何參數,都可以往後寫,例如:

void some_useful_func(int a, int b);

就寫成:

FAKE_VOID_FUNC(some_useful_func, int, int);

如果函式有返回值,例如:

int other_useful_adder(int a, int b);

可以使用:

FAKE_VALUE_FUNC(some_useful_adder, int, int);
預設支援最多 20 個參數,有需要更多的話,可以按照 Repo 中的步驟,利用 Ruby 重新產生客製化 fff.h。

如果最後一個參數是可變長度的參數 ...,只要加上 _VARARG 後綴即可,例如:

void add_log(const unsigned char *, ...);

就寫成:

FAKE_VOID_FUNC_VARARG(add_log, const unsigned char *, ...);

How fff works?

像是 C 這樣的編譯語言,一包原始碼變成執行檔的過程,大致上可分為:預處理(Preprocesss)、編譯(Compilation)、組譯/彙編(Assemble)、鏈接(Linking)四大階段,而跟你無關的Function,例如那些待測對象 .c 檔的其他原始檔,在編譯的前面步驟中其實只會保留一個 Tag,用以在 Linking 時辨識要去連結哪一個真正的 Definition。fff 的作用,就是提供一個假實作,你寫的每一行 FAKE_ Define,都會展開成一個假 Definition,讓 Linker 去連結,以產生 Test Doubles。而產生出來的假物件,fff 允許你透過一個後綴 _fake 的結構體去管理。

所以你想要 Stub 一個假 Function 的返回值的話,你可以直接指定 return_value

get_hw_status_fake.return_val = 2;

又或者你想要提供 Fake 實作的話,可以指定 custom_fake

void my_alloc_buffer(void * buffer, int size)
{
    buffer = malloc(size);
}

TEST(TestFunc, Case1) {
    // ...
    read_alloc_func_fake.custom_fake = my_alloc_buffer;
    // ...
}

如果你想知道測試對象有沒有 Call 到 Mock,可以檢查 call_count

TEST(TestFunc, Case1) {
    // ...
    very_important_func();
    EXPECT_EQ(add_log_fake.call_count, 1);
}

當然,也可以檢查帶入參數、歷史記錄等等,詳情請參閲 fff.h 的 README.md

 

References