概要
せっかくのお盆休みですが夏風邪っぽくやる気がわかないので、部屋でだらだらGoogle Mockをつつきました。
お仕事ではなんだかんだでCをひたすら使っているので、Google MockをCのコードに使う方法をまとめてみました。
今回は導入編です。
今回やってみること
- Google Testを用意する
- テストコードを書いてみる
- テスト環境を少し変えてみる
- Google MockをCに使ってみる
試した環境
- Windows 10 (Pro/64bit)
- Docker for Windows : 19.03.11
- Ubuntu : 18.04.3 LTS
- gcc : 7.4.0
内容
GoogleTestを導入する
この程度の記事はたくさんあるっぽいのでさらっと行きます。下記なども参考になると思います。
https://qiita.com/shohirose/items/30e39949d8bf990b0462
- Google Testを持ってくる
- Google Testをmakeする
とりあえずテストコードを書いてみる
こういうコードがあったとします。(Includeは省いたりしてますがよしなにやってます)
#include "mystr.h"
int test1(const char* str)
{
fprintf(stderr, "test print %s\n", str);
return str_analyze(str, 10);
}
int str_analyze(const char* str, int max)
{
int l;
for (l = 0; l < max; l++) {
if (str[l] == '\0') break;
}
return l;
}
このtest1()
関数の戻り値をテストするコードは下記のようになるでしょう。
TEST(sub_test, test01)
{
EXPECT_EQ(6, test1(": test"));
}
これらの配置やビルドは、とりあえずは下記のサイトのようにやります。
https://techblog.kayac.com/google-test
g++ -std=c++11 test/sub_test.cpp -I. -Isrc -Igoogletest/include -Lgoogletest -lgtest -lgtest_main -lpthread
このとおりにやるとa.out
ファイルがテストプログラムとして生成され、テストを実行できます。
配置を変えたりしてテスト環境を整える
このままだとGoogleTestのソースコードと開発コードが融合してしまうので、下記のように配置を変えてみます。
|
+- GoogleTest
| + ...
|
+- work
+ sub.c
+ sub.h
+ mystr.c
+ mystr.h
このときのビルドは上に書いたコマンドを少し変えればできますが、めんどくさいです。そこで最近仕事で覚えたsconsを使ってみます。
下記を適当に見ながら書きました。2
https://scons.org/doc/0.97/HTML/scons-user/x628.html
env = Environment()
env.Program(
target='sub_test',
# デバッグオプションはつけなくてもいいがあるとgdbでデバッグできる
CCFLAGS = '-g',
source = [
"sub.c",
"mystr.c",
"sub_test.cpp",
],
LIBPATH = [
"../googletest/lib",
"../googletest/googlemock/lib",
],
LIBS = [
'libgtest.a',
'libgtest_main.a',
'libgmock.a',
'libgmock_main.a',
'pthread',
],
CPPPATH = [
'.',
'../googletest',
'../googletest/googletest',
'../googletest/googletest/include',
'../googletest/googlemock',
'../googletest/googlemock/include',
],
)
まあ、これだと中間生成物がソースコードにまじるのであんまりよくないですが、今回はめんどくさくなったのでこのままです。
Google Mockを使う
ここからの内容は公式ドキュメントに加えて下記のページを参考にしています。
- https://futurismo.biz/archives/306
- https://stackoverflow.com/questions/31989040/can-gmock-be-used-for-stubbing-c-functions
なぜMockを使うのか
なぜMockを使うのかは公式ドキュメントの翻訳でも説明されています。
普段のプログラムでは,このインタフェースを本当に実装したものを利用するでしょう.テストでは,代わりにモック実装を利用します.これにより,どの描画プリミティブがプログラムから呼び出されたか,引数は何か,順番はどうか,といったチェックを簡単に行うことができます.このように書かれたテストは,非常にロバスト(マシンのアンチエイリアスが変化したせいで破綻したりはしません)で,解読とメンテナンスが容易(テストの目的は,画像ファイルではなくコードで表現されます),そして,テストの実施が各段に高速です.
個人的に、呼び出されたかとか順番はどうか、といったホワイトボックステスト的なチェックがMockを使う上で大事だと思っています。
さて、ここではmystrをMock化するモジュールとして、str_analyze()
のMockを作成します。が、Google MockはC++用なのでCにはそのままでは使えません。
インターフェースクラスとMockクラスを作る
公式ページには特に書いていないように見えますが、Google Mockを使うためにはインターフェースクラス3を作る必要があるようです。なので、mystrはCだけどC++だったら……という気持ちでヘッダーファイルを作ってみます。
class Mystr {
public:
Mystr(void){}
virtual ~Mystr(void){}
virtual int str_analyze(const char* str, int max) = 0;
};
これに対応するMockクラスを続けて書きます。
class MockMystr : public Mystr {
public:
MockMystr(){}
~MockMystr(){}
MOCK_METHOD2(str_analyze,
int(const char* str, int max));
};
テスト対象のコードからの呼び出しに対応させる
当然ですがCのコードからC++のクラスは呼び出せないですし、テスト対象のコードを書き換えるのもいまいちです。そこで上にあげた参考ページ2のようにMockを作ります。
extern MockMystr* mystr_mock;
extern "C" int str_analyze(const char* str, int max)
{
int ret = mystr_mock->str_analyze(str, max);
return ret;
}
extern "C"
はマングリングの抑制のためにだいたい必要だと思います。
ビルドを切り替えてMockを使えるようにする
ここまででMock化したかったstr_analyze()
関数のMock作成はだいたいできたので、これを使用するようにします。
まずstr_analyze()
の本体は見えないようにします。
source = [
"sub.c",
- "mystr.c",
+ #"mystr.c",
"sub_test.cpp",
],
そしてテストプログラムにMockの参照と設定を追加します。
#include "mock/mock_mystr.h"
MockMystr* mystr_mock;
// Mockを使うときにはなんかこういうMain関数が必要らしい
int main(int argc, char** argv)
{
::testing::InitGoogleMock(&argc, argv);
return RUN_ALL_TESTS();
}
TEST(sub_test, test01)
{
mystr_mock = new MockMystr();
const char* test_str = ": test";
int test_str_len = strlen(test_str);
EXPECT_CALL(mystr_mock, str_analyze(_, _))
.WillOnce(::testing::Return(test_str_len));
Mockの呼び出しに関するExpectationも上に書いてしまいました。EXPECT_CALL
ってやつですね。
これでテストができます。
まとめ
Google Testの導入をし、ちょっと配置をアレンジしてから、CでもGoogle Mockを使えるようにしました。CでGoogle Mockを使うための手法はいくつかありますが、今回は次の手順でCのGoogle Mockを使っています。
- Mock化したいモジュールのファイルに対応するインターフェースクラスを作る
- Mockとなる関数をexternな感じで作る
- Mockで使用するモジュールの実体をビルドから外す
- テストプログラムをMockに対応させる
インターフェースクラスを作ったりMock関数を作ったりが手間な感じですが、それぞれは単純な記述であるためもしかしたら自動化できるんじゃないかと思います。
おまけ: インターフェースクラスと vtable for エラー
今回のため、Mockで遊んでいたら下記のようなビルドエラーが出てしばらく苦しみました。
sub_test.o: In function `Mystr::Mystr()':
/var/work/c/test/work/./mystr.h:5: undefined reference to `vtable for Mystr'
sub_test.o: In function `Mystr::~Mystr()':
/var/work/c/test/work/./mystr.h:6: undefined reference to `vtable for Mystr'
あ、うん、vtableってときどき見るやつ、って感じでした。
これまでの人生ではなんだかんだ適当にいじってたらこのエラーが出なくなった!で過ごしてきましたが、今回はやる気を出して調べました。virtualって何なの? とかvtableって?という感じで。すると最終的に下記のページに到達しました。
まあこのページも答えではないのですが、要するに下記のように中途半端なインターフェースクラスを作るとこういうエラーを出してくるようです。
class Mystr {
public:
Mystr(void){};
virtual ~Mystr(void){};
virtual int str_analyze(const char* str, int max); // 純粋仮想関数になっていない
};
g++のエラーメッセージをそのまま読んでもインターフェースクラスが狂ってるからという結論に至れない気がしますが、これはこれで意味があるのだろうか……。
作ったテストコード
下記に公開しておきます。work01がmockなし、work02がmockありです。