#動機
ユニットテストは大きなシステムの部品の一つ一つを個別にテストすることなのに、大体一つのクラスは他のクラスの参照を持ってたりして、機能の一部は他のクラスに依存している。なので多くの場合純粋な意味での「ユニット」をテストすることは難しい。しかし、MockとDIはそれをある程度可能にしてくれる。この記事ではC++のMockフレームワークの中でも有名なgooglemockの使い方を紹介する。
#考え方
あるクラスTargetがクラスInjectedに対して集約関係、あるいは参照関係があるとする。Targetのある関数が想定通りに作用しないときに、その関数内でInjectedクラスのある関数を利用してたりすると、問題がどちら側のクラスにあるのかを調べるのが難しくなる。なので本物のInjectedの代わりにInjectMockというハリボテを使い、純粋にTargetクラスの関数が正常に作動してるかのみをテストする。
ただし、InjectMockを「使う」と単純にいっても、参照や集約関係があるクラスを簡単にすり替える事などできない。なので以下の方法ですり替える。
- 依存関係のあるクラスをコンストラクタで外から挿入(Inject)する形にする。
- クラスInjectedに純粋抽象クラス(IInjected)を与える。
- その純粋抽象クラスを継承したMockクラスを定義する。
TargetクラスのコンストラクタはIInjectedを取るようにし、テストの中ではInjectedMockを注入する。そしてプロダクション・コードではInjectedを注入する。これによりテスト内ではMockがInjectedの代わりにTargetクラスの内部に侵入できるので、それを利用して様々な事(何回注入されたBのどの関数が、どの順番で、どのような引数を渡されて呼ばれたかなど)をチェックできるようになる。
std::unique_ptr
このテクニックを用いるには、Targetのコンストラクタが引数としてIInjectedを取るため、多態性を使う必要が出てくる。つまりポインタ・参照渡しが必要となるが、コンストラクト後はTargetがInjectedを所有している形にしたい。この場合はunique_ptrを使い、std::moveでTargetにIInjected注入すると
- テストとプロダクション・コードで注入するクラスを変える
- 注入されたクラスはTargetに「所有」される
という二つが同時に達成できる。
#セットアップ
GmockはGtestをダウンロードすると付随してくる、googletest/googlemock/makeフォルダでmakeすればsampleがビルドされるので、あとはsampleファイルを変更するのが早い。
#例
ます、注入されるクラスの抽象クラスの定義とそのクラスへのunique_ptrの別名定義。
仮想デストラクタの定義を忘れるとGmockに怒られる。
class IInjected{
public:
virtual ~IInjected() = default;
virtual int doSomething(int x) = 0;
};
using InjectedPtr = std::unique_ptr<IInjected>;
次にMockを作成。上記クラスを継承し、Gmockの提供するマクロにより仮想関数をすべてラップする。このモックヘッダはgmock付随のPythonスクリプトにより自動生成できる。
class InjectedMock : public IInjected{
public:
InjectedMock(){}
MOCK_METHOD1(doSomething, int(int));
};
以下がテストのターゲットとなるクラスの定義。DIと多態性により純粋抽象クラスを介して依存を注入している。テストではモックを注入し、プロダクションコードでは本物のInjectedを注入する。useInjected関数内でinjectedMockのインスタンスを使っている。unique_ptrを使って、「注入するが、ポインタの所有権はTargetクラス(の中のstd::unique_ptrメンバ変数)に渡る事によって、コンストラクト後にはhas a関係が成立する」状態を作る。
class Target{
public:
Target(std::unique_ptr<IInjected> injected):injected_(std::move(injected)){}
void useInjected(){
auto y = injected_->doSomething(40);
}
private:
std::unique_ptr<IInjected> injected_;
};
以下テスト、injectionMockはTargetにmoveしているため、それ以降アクセスすることはできなくなる。以下のテストでは注入されたmockのuseInjectedの関数が40という引数でtarget.doAnything関数内で呼ばれている事を期待し、そのとおりであることをテストしている。
TEST(test, test){
std::unique_ptr<InjectedMock> injectionMock = std::make_unique<InjectedMock>();
EXPECT_CALL(*injectionMock, useInjected(40)).Times(1);
auto&& target = Target(std::move(injectionMock));
target.doAnything();
}