この記事のモチベーション
C/C++でのユニットテストを書くときテストに関係ないシンボルを解決しておかないとテスト実行できない。
これををみんなどう解決しているのか知りたい。
テスト対象に関係なくても全部必要なのもリンクする手間をかけてユニットテストのバイナリを作成するMakefileやvcxprojを書いているのだろうか?
それとも、C/C++のリンクの仕組を利用して、ユニットテスト用のバイナリを動かすためにテストダブルの様にシンボル定義してユニットテストを動かしているのだろうか?
どちらも手間のかかるつらい作業だが、そんな作業が必要ないようなプロダクトコードを書いて解決しているのだろうか?
実行時に未解決シンボルのエラーが出たときの解決方法
1. がんばってリンクする
エラーが出たシンボルの実体が定義されているオブジェクトやライブラリをリンクする。それがまた未定義のシンボルを引き連れてきたら、それも解決するライブラリをリンクするということを全て解決するまで再帰的に行う。
2. 自分でソースコード内に未解決シンボルを定義する
未解決のシンボルを自分でテストコード内に定義する。本物の実体があるオブジェクトはリンクしないか、それよりも先にリンクするなどする。リンカの挙動に気を付ける。
ユニットテストを記述するファイルの最初の方とか、未解決シンボル解決専用のソースに次のようなコードを書いてリンクする。
テストコードを実行したときに次のようにエラーが出て、
undefined symbol: my_undefined_foo_symbol
nm で次のように出力されていたら
U my_undefined_foo_symbol
ならば、ソースコードかデバッガーなどでシンボルの型や返り値と引数を調べて次のように定義する。
void my_undefined_symbol(){}
// 以下ユニットテストコードが続く
そうすれば本来のシンボルが定義されているオブジェクトはリンクしなくてよい。
3. リンカーでシンボルを定義する
3.1 GCCの-defsymオプションを使う
GCCなら-Wl,-defsym=my_undefined_symbol=0x0
と渡して解決する。
解決すべきシンボルが沢山あると書き切れずにつらい。
gcc ... -Wl,-defsym=my_undefined_symbol=0x0 ...
3.2 リンクスクリプトで定義してリンクする
ld で、シンボルを定義したファイルを作成しオブジェクトを作成する。
SECTIONS
{
. = 0x0;
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) }
_my_undefined_symbol = 0x0;
}
ld -T my_undefined_symbol.ld foo.o -o myunittest
のように書いて解決する。
4. テストする対象を絞る
4.1 リンクの工夫
foo.c bar.c のうち bar.c が外部依存で未解決シンボルが付いてくる場合、foo.o だけをリンクしてユニットテストバイナリを作る。
bar.o は別の手で未解決シンボルを解決する。もしくはユニットテストはあきらめる。
foo.o のみリンクしたら bar.o をリンクしたときより解決すべきシンボルが減るならそれでこの方法を選択してもいい。
テスト存在しなかったモジュールで、変更を加えたときfoo.c だけ変更したのでテストはそこだけには追加するとかなどでも使う。bar.cを変更していないのにシンボル解決がんばりたくない。
gcc ... foo.o foo_test.o -o unittest
4.2 includeでプロダクトコードをinclude
includeでプロダクトコードをincludeしてビルドしてソースコードのテストをする。
#include "../src/target.c"
// target.c 内のコードのテスト
5. プリプロセスで何とかする
ifdefやdefineなどマクロを駆使して未解決シンボルになる外部依存がユニットテストのときだけなくなるようにプロダクトコードを書き換える。
例えば #include "../src/foo.c" の直前にそのシンボルが呼ばれないようにするdefineを定義するなどしてごまかしを書ければ、プロダクトコードを書き換えなくて済むかもしれない。
ダミーのシンボル書いた方が簡単な気がする。
メンテナンスがダミーのシンボル定義書くのよりつらくなりそう。
6. もっと他の手段ある?
教えて😘
ユニットテストバイナリを作成する以外のアプローチ
a. リファクタする。
未解決シンボル少なくなるようにプロダクトコードをリファクタする。
b. テストのアプローチを変える
無理にシンボル解決してまでユニットテストにこだわらないで、別のレベルのテストでテストを担保する。
C. けど
けどユニットテスト書くときのシンボル解決の方法を知りたいのだ。
未解決シンボルの見つけ方
- ビルドして実行したときのエラーメッセージを見て、未解決シンボルがなくなるまで地道にシンボルを定義して解決していく。
-
nm
やdumpbin
を使用して未解決シンボルを見つけて実装する。- しかし本当にダミーのシンボル定義をしていい未解決シンボルか判断する一般的方法がない。
疑問
- みんな大量の未解決シンボルがあるとき、こんな地道な作業をしているのだろうか?
- 実はそんなに苦労せずに未解決シンボルを見つけてダミーの定義をする方法がないだろうか?
- そんな苦労しなくても普通に遅延束縛のようなアプローチがC/C++でもできるが私が知らないだけ?
- この方法で未解決シンボル解決しても、保守していく作業はどうしている?
- 入門用の記述だと未定義シンボル解決不要の内容しかないので実コードにあたるまで気付けない。
- 書籍もはっきり書いているものない気がする。リンクしたりテストダブルの記述の部分でみんな上のようなことを行えば未定義シンボルの解決できると気付くのだろうか?
- C++のカンファレンスでユニットテストのセッションわりとあってYouTubeに上がっているが、そういう未解決シンボル解決の話しているの見かけないので、みんなどうやってこういうアプローチ知るの?
- そういう未解決シンボルの解決方法を解説している書籍やブログを私が知らないだけ?
- 悩みはつきず、教えを乞いたい。