単体テストの効率化を考える(12)
同一ファイル内のサブ関数のスタブ置き換え

2022年7月3日

どもです。

久しぶりに、「単体テストの効率化を考える」シリーズです。
今回は、「同一ファイル内の関数のスタブへの置き換え」方法についてです。

「単体テストの対象関数」が呼び出す関数(「サブ関数」と呼称します)は、「スタブ」に置き換えてテストを実施することが一般的です。
本エントリでは、「テスト対象関数」と「サブ関数」が同じファイルに実装されている場合でも、この「置き換え」ができないか調査/検討してみたので、それについて書きます。

1. 結論

いきなり結論です。
できます。

調べてみると、同じようなことをやっている例は沢山ありました。
その中で、個人的にはコチラのサイトで紹介されている方法が、最適解であると考えています。

2. どうやる?

先に紹介したサイトでは、「スタブ関数を引数渡しにする方法」が紹介されています。
この方法は、そのタイトル(?)通り、「サブ関数」を引数で渡された関数ポインタで呼び出す、という方法です。
本エントリでは、以下のようなコードをベースとして少し実験してみます。

「単体テストの対象関数」およびその「サブ関数」が実装されたコード(test.c):

#include <stdio.h>

void function1(void)
{
	printf("This is %s\n", __FUNCTION__);
}

void function2(void)
{
	printf("This is %s\n", __FUNCTION__);
}

void target_function1(int a)
{
	printf("a = %d\n", a);
	
	function1();
	function2();
}
		

「サブ関数のスタブ」および「単体テストの対象関数」を呼び出すコード(main.c):

#include <stdio.h>

typedef void (*func_t)(void);
#define target_function1(a)	target_function1_stub_call(a, func_t function1, func_t function2)

#include "test.c"

void function1_stub(void)
{
	printf("This is %s\n", __FUNCTION__);
}

void function2_stub(void)
{
	printf("This is %s\n", __FUNCTION__);
}

int main()
{
	int a = 10;

	target_function1_stub_call(a, function1_stub, function2_stub);
	
	return 0;
}

このコードをビルドして実行すると、以下のようになります。



見てわかるように、テスト対象関数が「target_function1_stub_call」に置き換えられています。
また、「target_function1」が呼び出す関数が、それぞれ「function1_stub」と「function2_stub」に置き換えられています。
従って、ソースコードを変更することなく、テスト対象関数のサブ関数をスタブ関数に置き換えられていることが分かります。

3. どうなってる?

先述のコード/方法で、何がどうなっているかを簡単に解説します。

3.1. test.c

test.cについては、このファイルのみに着目した場合には、特筆すべきことはありません。

3.2. main.c

まず「main.c」ですが、先頭の「#define」では、「関数を置き換えるためのマクロ」が定義されます。
すなわち、このマクロが定義されているコード以降に定義されている「target_function1」という関数は、すべて「target_function1_stub_call」という名前に置換されます。
加えて置き換えの際に、引数に「func_t型の引数が追加されます。

main関数では、テスト対象関数を呼び出します。
しかし、呼び出す際のコードは「target_function1_stub_call」となります。
内容が前後してしまいますが、テスト対象関数「target_function1」はマクロにより、「target_function1_stub_call」に変換/置換されています。
そのため、この段階で既に「target_function1」という関数は存在せず、ビルドの際にエラーとなります。
(そんな関数、ねーよ!となるわけです。)

3.3. 全体としてどうなっている?

今回紹介しているコードですが、最も着目すべきは点は「#include “test.c"」であると考えています。
このコードにより、コンパイルの際にmain.cは、以下のようなコードとして解釈されます。
(便宜的に、#defineマクロの宣言を置換した形式を載せておきます。)

#include <stdio.h>
typedef void (*func_t)(void);
#define target_function1(a)	target_function1_stub_call(a, func_t function1, func_t function2)

void function1(void)
{
	printf("This is %s\n", __FUNCTION__);
}

void function2(void)
{
	printf("This is %s\n", __FUNCTION__);
}

void target_function1_stub_call(int a, func_t function1, func_t function2)
{
	printf("This is %s\n", __FUNCTION__);
	printf("a = %d\n", a);
	
	function1();
	function2();
}

void function1_stub(void)
{
	printf("This is %s\n", __FUNCTION__);
}

void function2_stub(void)
{
	printf("This is %s\n", __FUNCTION__);
}

int main()
{
	int a = 10;

	target_function1_stub_call(a, function1_stub, function2_stub);
	
	return 0;
}

main.cの先頭で宣言されている#defineにより、関数「target_function1」の名前が「target_function1_stub_call」に変換されます。
また、引数も「int a」のみから、サブ関数のスタブ関数への関数ポインタが加えられています。
テスト対象関数では、この関数ポインタが参照され、スタブ関数が実行されます。

4. 注意事項

最後に注意事項です。
上述のmain.cとtest.cをビルドする際は、「test.c」をビルド対象に含めてはいけません。
先述の通り、main.cにおいてtest.cを「include」しており、その内容はmain.cの中に展開されます。
そのためtest.cをビルド対象に含めた場合、当該のソースコードに含まれる関数が二重定義され、ビルドエラーとなります。

具体例としてコマンドラインからgccでビルドする場合、以下のように実施します。

gcc -o think_about_unit_test_012.exe main.c

5. まとめ

今回は、単体テストの効率化の一環として、テスト対象が実装されたソースコードを変更することなく、サブ関関数をスタブに置き換える方法を紹介しました。
紹介した方法の基本は、既にネットで公開されている方法です。
この方法を少し解析し、詳細に記載してみました。
なお本エントリでは、テスト対象関数の引数が1つだけとなっている例のみを表示しています。
2つ以上、またはない(void型)の場合については調査/検討ができていません。
それについては、次回のエントリに回したいと思います。

ではっ!