Google Mockを使ってみた

[Google Test](https://code.google.com/p/googletest/)とか[Google Mock](https://code.google.com/p/googlemock/)とか言うものがあることを知ったので、少し試してみた。

ドキュメントの日本語訳が、opencv.jpにあるので、そこを見ながら適当に。

簡単に言うと、Google TestがC++のテストフレームワークで、Google Mock はモックオブジェクトを簡単に記述できるフレームワーク。

良いテスト対象がなかったので、自分で作っている [select(2) wrapper](https://github.com/false-git/fselect) を対象にする。
テストを書いてみた結果は、同リポジトリの[gtestブランチ](https://github.com/false-git/fselect/tree/gmock)にある。

# Google Mock のビルド
Google Mockのプロジェクトページを見ると、ページ左にDownloadsと言うところがあるので、そこからzipファイルをダウンロードする。
この記事を書いているときの最新バージョンは、1.7.0らしい。

```shell-session
% 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.

と言われる。
とりあえずビルドに成功すると、以下のような状態になる。

<dl>
<dt>gmock-1.7.0/include</dt>
<dd>gmockのヘッダファイル</dd>
<dt>gmock-1.7.0/lib</dt>
<dd>gmockのライブラリ(libgmock.la)</dd>
<dt>gmock-1.7.0/gtest/include</dt>
<dd>gtestのヘッダファイル</dd>
<dt>gmock-1.7.0/gtest/lib</dt>
<dd>gtestのライブラリ(libgtest.la)</dd>
</dl>
Mac OS Xで試したのだけれど、libgmock.la は標準のldでは読めないようで、そのままではリンクできなかった。(gmock-1.7.0/lib/.libs/libgmock.dylibならばリンクできるが、実行時に探す場所がconfigure の --prefixで指定した場所になるため、make installができない状態では使いにくい)

# テスト対象とモック対象を決める
今回の対象はfselectと言うクラスで、システムコールselect, popen, read, write等を使っている。
fselectを使うコードはないので、fselectをモック化しても意味がない。
そこで、今回はfselect自身をテスト対象とし、select(2)をモック化することにする。

# モック化のためのソース修正
gmockは、インターフェイスと実装クラスがあるようなケースで、インターフェイスをモック化するものなので、システムコールであるselect(2)をそのままモック化することはできない。
gmockのクックブックには、[フリー関数をモック化する](http://opencv.jp/googlemockdocs/cookbook.html#cookbook-mocking-free-functions)と言うセクションがあり、そこによると関数をインターフェイス化し、具象クラスを派生化させ、インターフェイスをモック化しろとある。
今回の場合わざわざインターフェイスと具象クラスに分けなくても良いだろうと思ったので、Selectと言うクラスをデフォルト実装付きのインターフェイスとしてくくりだした(select.h/select.cc)
後は、もともとあるfselectと言うクラスが、システムコールselect(2)を呼ぶ代わりに新しいSelect(かそのサブクラス)を呼んでくれれば良い。

クックブックの「非仮想メソッドをモック化する」には、templateを使えば良いんじゃないの？的なことが書いてあるのだが、fselectをtemplateクラスにしてしまうと、実装をほとんどヘッダに書かないといけなくなるため、苦肉の策としてコンストラクタで`Select*`を受け取ることにした。省略すると、今回分離したSelectを内部で生成する。

# モック生成
後は、モッククラスを作る。(mock_select.h)
短いのでソースを載せてしまうと、

```cpp: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の返値を偽造するケース

```cpp:gtest.cc
// 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`を使うと好きなことができるらしい。

```cpp:gtest.cc
// 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を使ってあげると、引数の変更でもなんでもできるようになる。

# テストのビルド

```shell-session
% 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をリンクしてやれば良い。

# テストの実行

```shell-session
% ./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に限った話ではなく、自動テスト一般に言えることなので、普段からテストを意識したコードを書けるかどうかが効いてくるんだろう。(恥ずかしながら私は全然駄目だ)