Google TestとかGoogle Mockとか言うものがあることを知ったので、少し試してみた。
ドキュメントの日本語訳が、opencv.jpにあるので、そこを見ながら適当に。
簡単に言うと、Google TestがC++のテストフレームワークで、Google Mock はモックオブジェクトを簡単に記述できるフレームワーク。
良いテスト対象がなかったので、自分で作っている select(2) wrapper を対象にする。
テストを書いてみた結果は、同リポジトリのgtestブランチにある。
Google Mock のビルド
Google Mockのプロジェクトページを見ると、ページ左にDownloadsと言うところがあるので、そこからzipファイルをダウンロードする。
この記事を書いているときの最新バージョンは、1.7.0らしい。
% unzip gmock-1.7.0.zip
% cd gmock-1.7.0
% ./configure
% make
ちなみに、gmockはHomebrewのblacklist.rbに載っていて、
Installing gmock system-wide is not recommended; it should be vendored
in your projects that use it.
と書いてある。
gmockのMakefileでも、make installしようとすると
'make install' is dangerous and not supported. Instead, see README for how to integrate Google Test into your build system.
と言われる。
とりあえずビルドに成功すると、以下のような状態になる。
- gmock-1.7.0/include
- gmockのヘッダファイル
- gmock-1.7.0/lib
- gmockのライブラリ(libgmock.la)
- gmock-1.7.0/gtest/include
- gtestのヘッダファイル
- gmock-1.7.0/gtest/lib
- gtestのライブラリ(libgtest.la)
テスト対象とモック対象を決める
今回の対象はfselectと言うクラスで、システムコールselect, popen, read, write等を使っている。
fselectを使うコードはないので、fselectをモック化しても意味がない。
そこで、今回はfselect自身をテスト対象とし、select(2)をモック化することにする。
モック化のためのソース修正
gmockは、インターフェイスと実装クラスがあるようなケースで、インターフェイスをモック化するものなので、システムコールであるselect(2)をそのままモック化することはできない。
gmockのクックブックには、フリー関数をモック化すると言うセクションがあり、そこによると関数をインターフェイス化し、具象クラスを派生化させ、インターフェイスをモック化しろとある。
今回の場合わざわざインターフェイスと具象クラスに分けなくても良いだろうと思ったので、Selectと言うクラスをデフォルト実装付きのインターフェイスとしてくくりだした(select.h/select.cc)
後は、もともとあるfselectと言うクラスが、システムコールselect(2)を呼ぶ代わりに新しいSelect(かそのサブクラス)を呼んでくれれば良い。
クックブックの「非仮想メソッドをモック化する」には、templateを使えば良いんじゃないの?的なことが書いてあるのだが、fselectをtemplateクラスにしてしまうと、実装をほとんどヘッダに書かないといけなくなるため、苦肉の策としてコンストラクタでSelect*
を受け取ることにした。省略すると、今回分離したSelectを内部で生成する。
モック生成
後は、モッククラスを作る。(mock_select.h)
短いのでソースを載せてしまうと、
#ifndef __MOCK_SELECT_H
#define __MOCK_SELECT_H
#include "select.h"
#include "gmock/gmock.h"
namespace wl {
class MockSelect: public Select {
public:
MOCK_METHOD5(select, int(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout));
};
}
#endif // __MOCK_SELECT_H
テストコード
テストコードは、gtest.cc
わかりやすいところで、selectの返値を偽造するケース
// 0(stdin)を監視し、selectがエラーを返すケース
TEST(FSelectTest, select_02) {
wl::MockSelect *mock = new wl::MockSelect;
EXPECT_CALL(*mock, select(4, testing::_, testing::_, testing::_, 0))
.Times(AtLeast(1))
.WillOnce(Return(-1));
wl::fselect fselect(mock);
bool stop;
fselect.read_watch(0);
int result = fselect.select(stop);
ASSERT_EQ(-1, result);
}
EXPECT_CALLで、selectが1回は呼ばれること、呼ばれたときには-1を返すことを記述している。
そして、実際にfselect.select()を呼んだ後で、ASSERT_EQで返値のチェックをしている。
続いて、モックで引数を更新する例。
クックブックには、「副作用をモック化する」と言うセクションがあり、SetArgPointee
が紹介されているが、selectの引数はfd_setのポインタであり、ポインタが指す構造体のメンバの更新には使えそうにない(たぶん・・・)
そこで、同じくクックブックの「関数/メソッド/ファンクタを Action として利用する」のところを読むと、Invoke
を使うと好きなことができるらしい。
// 0(stdin)を監視し、0が読み込み可能になるケース
TEST(FSelectTest, select_01) {
wl::MockSelect *mock = new wl::MockSelect;
EXPECT_CALL(*mock, select(4, testing::_, testing::_, testing::_, 0))
.Times(AtLeast(1))
.WillOnce(Invoke([](int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout) {
FD_ZERO(readfds);
FD_ZERO(writefds);
FD_ZERO(errorfds);
FD_SET(0, readfds);
return 1;
}));
wl::fselect fselect(mock);
bool stop;
fselect.read_watch(0);
int result = fselect.select(stop);
ASSERT_EQ(1, result);
ASSERT_EQ(true, fselect.read_isready(0));
}
そこで、こんな感じにlambdaを使ってあげると、引数の変更でもなんでもできるようになる。
テストのビルド
% make
c++ -I../gmock-1.7.0/include -I../gmock-1.7.0/gtest/include -g --std=c++11 -pthread -Wall -c -o gtest.o gtest.cc
c++ -I../gmock-1.7.0/include -I../gmock-1.7.0/gtest/include -g --std=c++11 -pthread -Wall -c -o fselect.o fselect.cc
c++ -I../gmock-1.7.0/include -I../gmock-1.7.0/gtest/include -g --std=c++11 -pthread -Wall -c -o select.o select.cc
c++ -o gtest gtest.o fselect.o select.o ../gmock-1.7.0/src/gmock-all.o ../gmock-1.7.0/gtest/src/gtest-all.o
READMEの通り、gmockとgtestのincludeにインクルードパスを通して、gmock-all.oとgtest-all.oをリンクしてやれば良い。
テストの実行
% ./gtest
[==========] Running 3 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 3 tests from FSelectTest
[ RUN ] FSelectTest.select_01
[ OK ] FSelectTest.select_01 (1 ms)
[ RUN ] FSelectTest.select_02
[ OK ] FSelectTest.select_02 (0 ms)
[ RUN ] FSelectTest.select_03
[ OK ] FSelectTest.select_03 (0 ms)
[----------] 3 tests from FSelectTest (1 ms total)
[----------] Global test environment tear-down
[==========] 3 tests from 1 test case ran. (1 ms total)
[ PASSED ] 3 tests.
gmockだけでなくgtest自体も今回初めて触ったので、まだ良くわからないけど、./gtest --help とかやるといろいろオプションが出てきたり、xml出力もできたりするようなので、もうちょっと調べてみようと思う。
gmockについては、かなり綺麗にモジュールが切り分けられていないと、テストは難しいと感じた。まあ、それはgmockに限った話ではなく、自動テスト一般に言えることなので、普段からテストを意識したコードを書けるかどうかが効いてくるんだろう。(恥ずかしながら私は全然駄目だ)