最近、ユニットテストについて色々試してたので、記事として書いておきます。
やりたいこと
Linux環境のC言語で、ファイルローカルのstatic関数に対するユニットテストを書きたい。
ただ、テスト対象はファイルローカルなので、ユニットテストのmain関数のファイルとは異なるため、なんとかこねくり回さないとテストの実行ファイルをリンクできない。
想定ファイル構成
プロダクトコードは、以下のような構成を想定します。
ユニットテストを行いたい対象は、func.cのstatic_func関数です。
また、ユニットテストはCppUTestを利用することを想定しています。
プロダクトコードソース
#include <stdio.h>
static int static_func(int param) {
printf("static_func %d\n", param);
return param;
}
int public_func(int param) {
printf("public func %d\n", param);
return static_func(param);
}
int public_func(int param);
#include "func.h"
int main(int argc, char **argv) {
return public_func(1);
}
ユニットテストコードソース
#include <CppUTest/CommandLineTestRunner.h>
#ifdef __cplusplus
extern "C" {
extern int static_func(int);
}
#endif
TEST_GROUP(TestFuncGroup)
{
TEST_SETUP()
{
}
TEST_TEARDOWN()
{
}
};
TEST(TestFuncGroup, static_func)
{
int ret = static_func(1);
CHECK_EQUAL(1, ret);
}
int main(int argc, char **argv)
{
return CommandLineTestRunner::RunAllTests(argc, argv);
}
プロダクトコンパイル
$ gcc func.c main.c -o exec.out
$ ./exec.out
public func 1
static_func 1
ユニットテストコンパイル(リンクエラー)
$ gcc -c func.c
$ g++ -c utest_func.cpp
$ g++ -o utest.out func.o utest_func.o -lCppUTest
utest_func.o: 関数 `TEST_TestFuncGroup_static_func_Test::testBody()' 内:
utest_func.c:(.text+0x26): `static_func' に対する定義されていない参照です
collect2: error: ld returned 1 exit status
方針
ユニットテストを書く時に、なるべく以下の方針で行えるようにしたい。
- プロダクトコードにあまり手が入らない
- ユニットテストを書くための準備が少ない
- 楽したい
考えたこと
static関数に対して、ユニットテストを行う方法として、以下を考えました。
- staticキーワードを別のマクロにして、ユニットテストを行うときにstaticを外す
- リンカスクリプトをいじって、static_func関数の関数アドレスを取得する
- コンパイルしたfunc.cのオブジェクトをいじって、static_func関数をLOCALからGLOBALにする
- 別途グローバル変数を追加して、static_func関数の関数アドレスを公開する
1. staticキーワードを別のマクロにする
staticキーワードをunit_staticにして、ユニットテスト用のdefineがあるときは、staticを消す、
という方法です。
マクロによる修正
#include <stdio.h>
#ifdef UNIT_TESTING
#define unit_static
#else
#define unit_static static
#endif
unit_static int static_func(int param) {
printf("static_func %d\n", param);
return param;
}
int public_func(int param) {
printf("public func %d\n", param);
return static_func(param);
}
コンパイル
defineを有効にしてコンパイルします。
$ gcc -c func.c -DUNIT_TESTING
$ g++ -o utest.out func.o utest_func.o -lCppUTest
$ ./utest.out
static_func 1
.
OK (1 tests, 1 ran, 1 checks, 0 ignored, 0 filtered out, 0 ms)
テストできました。
マクロの利用する方法について
マクロを利用する方法の場合、以下のメリット、デメリットがあります。
- 修正が見てわかりやすい
- プロダクトコードに対して、staticキーワードの変更が必要になる
- (マクロの展開後の変更はないが)プロダクトコードに修正が入る
- テスト対象が多いと各関数のstaticを修正する、修正位置がまとまってない
2. リンカスクリプトをいじる
端的に言うと、駄目でした。
ローカルシンボルのアドレスを取ってくることができませんでしたし、リンカスクリプトを書くのも楽じゃない。
3. オブジェクトをいじる
コンパイルしたfunc.oのシンボルテーブルをいじって、LOCALのシンボルから、GLOBALに変更し、リンクさせる方法です。
必要なもの
オブジェクトファイルをいじるためには、objcopyが必要になります。
また、objcopyでも、"globalize-symbol"が利用できるバージョンが必要です。
確認
まずは、普通にコンパイルしたfunc.oを見てみます。
$ readelf -s func.o
Symbol table '.symtab' contains 13 entries:
番号: 値 サイズ タイプ Bind Vis 索引名
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS func.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 38 FUNC LOCAL DEFAULT 1 static_func
7: 0000000000000000 0 SECTION LOCAL DEFAULT 7
8: 0000000000000000 0 SECTION LOCAL DEFAULT 8
9: 0000000000000000 0 SECTION LOCAL DEFAULT 6
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
12: 0000000000000026 45 FUNC GLOBAL DEFAULT 1 public_func
当たり前ですが、static_funcはLOCALになっています。
objcopyによる改変
objcopyを使って、static_func関数をGLOBALに変更します。
改変したオブジェクトはufunc.oとしています。
$ objcopy --globalize-symbol static_func func.o ufunc.o
$ readelf -s ufunc.o
Symbol table '.symtab' contains 13 entries:
番号: 値 サイズ タイプ Bind Vis 索引名
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS func.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 6
9: 0000000000000000 38 FUNC GLOBAL DEFAULT 1 static_func
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
12: 0000000000000026 45 FUNC GLOBAL DEFAULT 1 public_func
シンボルの位置は変わってますが、GLOBALになりました。
コンパイル
改変したufunc.oを使用してコンパイルします。
$ g++ -o utest.out ufunc.o utest_func.o -lCppUTest
$ ./utest.out
static_func 1
.
OK (1 tests, 1 ran, 1 checks, 0 ignored, 0 filtered out, 0 ms)
テストできました。
オブジェクトをいじる方法について
オブジェクトファイルをいじる方法の場合、以下のメリット、デメリットがあります。
- プロダクトコードに対する修正が、一切不要
- objcopyのような、LOCALからGLOBALに改変できるものが必要
4. グローバル変数を追加
ローカル関数のアドレスを格納する、グローバル変数を作成して行う方法です。
グローバル変数を追加する方法の修正
まず、ローカル関数のアドレスを格納する、グローバル変数を作成するためのマクロを書きます。
これは、ローカル関数の戻り値の型、関数名、引数から、
同じ型で、"utest_"を先頭に追加した名前の変数を作成するものと、
ユニットテスト本体側に、作成した変数をexternで入れて、元の名前に戻しているものです。
#define UTEST_STATIC_TO_GLOBAL_FUNC(ret_type, func_name, ...) \
ret_type (*utest_##func_name) (__VA_ARGS__) = func_name
#define UTEST_EXTERN_STATIC_FUNC(ret_type, func_name, ...) \
extern ret_type (*utest_##func_name) (__VA_ARGS__);\
ret_type (*func_name) (__VA_ARGS__) = utest_##func_name
unit_test.hのマクロを使用してfunc.cに処理を追加します。
追加するコードは、プロダクトコードとは独立して、ユニットテスト用のdefineのある時のみ有効となるように書けます。
#include <stdio.h>
static int static_func(int param) {
printf("static_func %d\n", param);
return param;
}
int public_func(int param) {
printf("public func %d\n", param);
return static_func(param);
}
#ifdef UNIT_TESTING
#include "unit_test.h"
UTEST_STATIC_TO_GLOBAL_FUNC(int, static_func, int);
#endif
ユニットテスト本体側では、普通にstatic関数をextern宣言していた部分を、EXTERNのマクロを使用するように変更します。
修正点は、#ifdef __cplusplusのマクロ内のみです。
#include <CppUTest/CommandLineTestRunner.h>
#ifdef __cplusplus
extern "C" {
#include "unit_test.h"
UTEST_EXTERN_STATIC_FUNC(int, static_func, int);
}
#endif
// 以下修正なし
コンパイル
func.cで、defineを有効にしてコンパイルします。
$ gcc -c func.c -DUNIT_TESTING
$ g++ -c utest_func.cpp
$ g++ -o utest.out func.o utest_func.o -lCppUTest
$ ./utest.out
static_func 1
.
OK (1 tests, 1 ran, 1 checks, 0 ignored, 0 filtered out, 0 ms)
テストできました。
グローバル変数を追加する方法について
グローバル変数を追加する方法の場合、以下のメリット、デメリットがあります。
- プロダクトコードに対して修正が必要になる
- ただし、修正はユニットテスト用のdefine内に閉じている
- 修正位置がまとまってる
- objcopyのような、LOCALからGLOBALに改変できるものは不要
- ちょっとややこしい
まとめ
4つの方法(1つは失敗)で、static関数のユニットテストを行う方法をやってみました。
実際にどれを使うかですが、一番楽なのは、オブジェクトをいじる方法ですが、
objcopyコマンドが必要なので、linux環境以外では難しいかもしれません。
また、コンパイル後のオブジェクトファイルを直接改変するため、微妙に抵抗が大きいです。
なので、オブジェクトファイルを改変してもいい場合は、オブジェクトをいじる方法で、
ダメなら、グローバル変数を追加する方法でやる感じですかね。
staticキーワードを別のマクロにする方法は、プロダクトコードに直接修正が入るので、あまりやりたくないかな。