10
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

C言語のテストでスタブ関数を使うためのアイデア

Posted at

TL;DR

teratilのこのスレをまとめたものです。

マクロのトークン連結演算子(##)を使って関数名にソースファイル上の行数を付加することで、定義されている既存関数が呼ばれないようにします。

背景

既存のソースに対してテストプログラムを書く必要があり、異常系の確認のために関数をスタブに置き換えたいという状況がありました。
オブジェクト指向的な言語ではDI(Dependency Injection)というデザインパターンとしてコード例も簡単に見つかりますが、C言語での書き方はすぐに見つけられず、しばらくハマってしまいましたので共有したいと思います。

特に下記のような状況では有用ではないかと思います。

  • 既存ソースは変更できない
  • 置き換えたい関数が内部関数として同じファイルから呼び出されている

アイデアの概要

基本的なアイデアは、既存関数を別名に置き換え、テストプログラム中のスタブを呼び出すことです。マクロでの実装を考えます。
イメージとして、下記のようなテストプログラムを考えます。

TestMain.c
/* something to replace the target_function to stub_function */
#define HOGE_HOGE foo_bar

int stub_function(int a, int b);

#include "TestTarget.c"

int stub_function(int a, int b)
{
    /* always return zero */
    return 0;
}

int main(void)
{
    target_main();
    return 1;
}
TestTarget.c
#include <stdio.h>

int target_function(int a, int b);

void target_main(void)
{
    int func_result;
    
    func_result = target_function(1, 2);
    
    printf("The result is %d\n", func_result);
}

int target_function(int a, int b)
{
    return a + b;
}

関数の内容を置き換える方法

関数名にソースファイル上の行数を付加する

以下のマクロを使うことで、TestTarget.c内の関数名にソースファイル上の行数を付加します。

TestMain.c(抜粋)
#define target_function(a, b) target_function_(__LINE__, a, b)
#define target_function_(line, a, b) target_function__(line, a, b)
#define target_function__(line, a, b) target_function_##line(a, b)

マクロが展開されることによって、TestTarget.cは以下のようになります。

TestTarget.c(マクロ展開後)
#include <stdio.h>

int target_function_3(int a, int b);

void target_main(void)
{
    int func_result;
    
    func_result = target_function_9(1, 2);
    
    printf("The result is %d\n", func_result);
}

int target_function_14(int a, int b)
{
    return a + b;
}

さらに、呼び出し部分でスタブ関数を呼び出すために行数付きの関数名をスタブ関数に変換します。
定義部分の関数名は変換せずにおいておきます。

TestMain.c(抜粋)
#define target_function(a, b) target_function_(__LINE__, a, b)
#define target_function_(line, a, b) target_function__(line, a, b)
#define target_function__(line, a, b) target_function_##line(a, b)

#define target_function_3(a, b) stub_function(a, b)
#define target_function_9(a, b) stub_function(a, b)

TestTarget.c(マクロ展開後)
#include <stdio.h>

int stub_function(int a, int b);

void target_main(void)
{
    int func_result;
    
    func_result = stub_function(1, 2);
    
    printf("The result is %d\n", func_result);
}

int target_function_14(int a, int b)
{
    return a + b;
}

デメリットとして、テスト対象のソース(TestTarget.c)に変更があった場合、置き換えたい関数(target_function)に関係のない変更であってもテストプログラムのマクロを変更する必要があることです。
しかし、ソースを変更した場合にスタブ関数の内容を見直す必要がある場合も考えられます。見直しが必要かどうかを含めて判断するきっかけとして、ビルドエラーを発生させることで見直しを強制したいので、私はこちらの方法を採用しました。

※本当は、予期せぬ動きをした時にはテストがFailするようにテストプログラムを書くべきかと思いますが。

関数ポインタを渡す

テストプログラムの仕様を含めてちゃんと管理されている場合は、こちらの方法がメンテナンスコストが低いので良いと思います。

テストしたい関数を呼び出している関数(target_main)を関数ポインタを引数に取るように変換する方法です。

TestMain.c
typedef void (*func_t)(void);
#define target_main(a) test_stubcall(func_t target_function)

int stub_function(int a, int b);

#include "TestTarget.c"

int stub_function(int a, int b)
{
    /* always return zero */
    return 0;
}

int main(void)
{
    test_stubcall(stub_function);
    return 1;
}
TestTarget.c(マクロ展開後)
#include <stdio.h>

int target_function(int a, int b);

void test_stubcall(func_t target_function)
{
    int func_result;
    
    func_result = target_function(1, 2);
    
    printf("The result is %d\n", func_result);
}

int target_function(int a, int b)
{
    return a + b;
}

target_functionという名前の関数ポインタを受け取るように変換することで、target_main関数の中のtarget_functionの呼び出しを関数ポインタ経由での呼び出しに変換しています。
行数を含まないため、元のソースファイル(TestTarget.c)の変更にも比較的強いです。

関数の呼び出し部分をマクロで定義する方法

今回は既存ソースを変更しない制限がありましたが、既存ソースを変更できるならこの方法が一番スマートだと思います。

target_functionの呼び出し部分を関数マクロにしておいて、後からスタブ関数にも置き換えられるように変更しています。

TestMain.c
#define CALL_FUNCTION(a, b) stub_function(a, b)

int stub_function(int a, int b);

#include "TestTarget.c"

int stub_function(int a, int b)
{
    /* always return zero */
    return 0;
}

int main(void)
{
    target_main();
    return 1;
}
TestTarget.c
#include <stdio.h>

#ifndef CALL_FUNCTION
#define CALL_FUNCTION(a, b) target_function(a, b)
#endif

int target_function(int a, int b);

void target_main(void)
{
    int func_result;
    
    func_result = CALL_FUNCTION(1, 2);
    
    printf("The result is %d\n", func_result);
}

int target_function(int a, int b)
{
    return a + b;
}
10
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?