こつつみ

コツコツ積み上げる

C言語の単体テストをする上で最強のフレームワーク

C言語単体テストをする上で最強のフレームワークはこちら

  • GoogleTest

github.com

  • FFF (Fake Function Framework)

github.com

Google Test

こちらは使っている人も多いだろう。

GoogleTestを使うメリットとしては何よりも、エラーが出た際にOSSなのでWebでの情報が多いことだ。

私の部署では、以前は CPPTEST と呼ばれる有償のソフトを使っていたがWebに情報がなくて辛い思いをした。

GoogleTestなら日本語のドキュメントもあるし、色々な使い方ができる。

opencv.jp

opencv.jp

基本的には以下のように TEST_P を使っていくのが良いだろう。(個人的意見です) 今回テストする関数は足し算をするだけなので、TEST() で十分ですが参考のためTEST_P() を使ったやり方でやっています。

// add.c
int add(int x, int y) {
    return x + y;
}


// test_add.c
typedef struct {
    int ret;
    int x;
    int y;
} AddTest;

AddTest add_test_table[] =
{
    /*  [OUT]ret,  [IN]x,  [IN]y */
    {   5,                2,        3  },
    {   1000,          500,     500  },
    {   -10,             10,        -20  },
};

class ParamAddTest : public testing::Test, public testing::WithParamInterface<AddTest> {
public:
    void SetUp()
    {
        // 初期値みたいの設定する必要があれば
    }
    void TearDown()
    {
        // 終了処理みたいのがあれば
    }
};

TEST_P(ParamAddTest, add)
{
    int x = GetParam().y;
    int y = GetParam().y;
    EXPECT_EQ(GetParam().ret, add(x, y));
}

INSTANTIATE_TEST_CASE_P(ParamTest, ParamAddTest, testing::ValuesIn(add_test_table));

add_test_table の1行ごとに合計テストが3回実行される。

FFF

これは簡単にスタブを作ってくれるフレームワークだ。

使い方はとっても簡単。 #include "fff.h" をするだけだ。

https://github.com/meekrosoft/fff#hello-fake-world

の例を取り上げる。

https://github.com/meekrosoft/fff#hello-fake-world

Hello Fake World!

例えば、組み込み型のユーザーインターフェースをテストしていて、フェイクを作成したい機能があるとします。

// UI.c
void DISPLAY_init();
// test.cpp
#include "fff.h"
DEFINE_FFF_GLOBALS;

// テストスイートでこのための偽の関数を定義する方法は次のとおりです。
FAKE_VOID_FUNC(DISPLAY_init);

// そして、テストは以下のようになります。
TEST_F(GreeterTests, init_initialises_display)
{
    UI_init();
    ASSERT_EQ(DISPLAY_init_fake.call_count, 1);
}

さて、ここで何が起こったのでしょうか?このフレームワークを使うために必要なことは、fff.hをダウンロードして、テストスイートに含めることだけです。

魔法はFAKE_VOID_FUNCにあります。これは、ゼロの引数を持つvoidを返す関数を定義するマクロを展開します。また、フェイクに関するすべての情報を含むstruct "function_name"_fakeも定義されています。例えば、DISPLAY_init_fake.call_countは、偽の関数が呼ばれるたびにインクリメントされます。

フードの下では、次のような構造体が生成されます。

typedef struct DISPLAY_init_Fake {
    unsigned int call_count;
    unsigned int arg_history_len;
    unsigned int arg_histories_dropped;
    void(*custom_fake)();
} DISPLAY_init_Fake;
DISPLAY_init_Fake DISPLAY_init_fake;

戻り値があるものについても似たようにできる。

そして、戻り値には任意の値が設定できるのだ。

GoogleTest + FFF でのテスト

GoogleTest と FFF を組み合わせることで、どんなテストでも対応できるようになる。

HelloWorldとデータを送信する関数を例に単体テストを作成していく。 (ゴミみたいなコードですけど...)

// send_hello_world.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>

int send_hello_world(int fd) {
    if (fd < 0)
        return -1;
  
    //データ送信
    char s_str[] = "HelloWorld!";         //送信データ格納用
    int ret = send(fd, s_str, 12, 0);
    if (ret < 0)
        printf("send ERROR: %s", strerror(errno));
    return ret;
}


// send_hello_world.h
int send_HelloWorld(int fd);


// stub_socket.h
#include "fff.h"

// send関数をスタブ化する
FAKE_VALUE_FUNC(int, send, const void *, size_t, int);


// test_send_hello_world.c
#include "fff.h"
DEFINE_FFF_GLOBALS;

#include "gtest/gtest.h"

extern "C" {
#include "send_hello_world.h"
#include "stub_socket.h"
}

typedef struct {
    int ret;
    int fd;
    ssize_t send_return;
} SendHelloWorldTest;

SendHelloWorldTest send_hellow_world_test_table[] =
{
    /*  [OUT]ret,  [IN]fd,  [IN]send_return */
    // 成功
    {   12,              2,           12  },
    // fdが不正
    {   -1,              -1,          12  },
    // sendに失敗
    {   -1,              5,            -1  },
};

class ParamSendHelloWorldTest : public testing::Test, public testing::WithParamInterface<AddTest> {
public:
    void SetUp()
    {
        RESET_FAKE(send);
    }
};

TEST_P(ParamAddTest, add)
{
    int fd = GetParam().fd;
    send_fake.return_val = GetParam().send_return;
    EXPECT_EQ(GetParam().ret, send_hello_world(fd));
}

INSTANTIATE_TEST_CASE_P(ParamTest, ParamSendHelloWorldTest, testing::ValuesIn(send_hellow_world_test_table));

これでsocketの戻り値をコントロールできるので、テストができるようになった。

テストを書こう

自分の職場は単体テストを書かなきゃと言っているものの誰も書いていない状態です。

どんなに忙しくても単体テストは書くべきです。

これを見てC言語のテストを書く人が増えることを祈っています。

テストの重要性については別の記事でも読んでください。