pepe_la_phew's diary

LL系の話など

ifstreamをモック化

gmockを使えば他クラス実装への依存を減らせるので有り難いのですが、今回のお題であるifstreamのように非virtual・既存・変更不可なクラスをモック化したい場合にはちょっとしたトリックが必要です。
トリックの内容は公式のクックブックの通りですが、以下の3点です:

  • モック化したいクラスを継承するのではなく、別クラスとして定義する。
    • 下記の例ではifstreamの継承ではなくMockStreamを作成。
  • テスト対象クラスをテンプレートクラスとする。テンプレートパラメータとしてモック化対象クラスを指定する。
    • 例ではBinaryReaderクラスをテンプレート化。テンプレートパラメータとして、プロダクトコードではifstreamを、テストコードではMockStreamを指定。
  • モック化対象クラスを参照ではなくポインタで保持する。

以下のサンプルは、ifstreamを使ってバイナリデータを読むためのクラスと、そのテストコードです。

#include <gtest/gtest.h>
#include <gmock/gmock.h>
using namespace ::testing;

class MockStream {
public:
    MOCK_METHOD2(read, MockStream&(char* s, int n));
};

template<class T>
class BinaryReader {
    T* const m_stream;
public:
    BinaryReader(T& stream)
        : m_stream(&stream)
    {}

    template<typename U> void read(U& data) {
        m_stream->read(reinterpret_cast<char*>(&data), sizeof(data));
    }
};
TEST(BinaryReader, read_char) {
    StrictMock<MockStream> stream;

    EXPECT_CALL(stream, read(A<char*>(), TypedEq<int>(1)))
        .Times(1)
        .WillOnce(DoAll(SetArgPointee<0>(0x01),
                        ReturnRef(stream)));

    BinaryReader<MockStream> reader(stream);
    char data;
    reader.read(data);

    ASSERT_EQ(char(0x01), data);
}

モック対象クラスをT*とポインタで保持してある点について補足を。
ポインタは気持ち悪いので参照で保持しておきたいところです。こんな感じですね。

template<class T>
class BinaryReader {
    T& m_stream;
public:
    BinaryReader(T& stream)
        : m_stream(stream)
    {}
};

これをコンパイルするにはモッククラスにコピーコンストラクタを定義する必要があります。
しかしEXPECT_CALLが見ているのはstreamであってコピー先のインスタンス(BinaryReader::m_stream)とは別物なので、モックがうまく動きません。
結果としてポインタで持たざるを得ないということになります。

…この結論に辿り着くまで随分時間がかかってしまったので、以下のようにモックを使わず直接ファイルを読むテストを書く方が早いと思ってしまいました :p

TEST(BinaryReader, read_char) {
    std::ifstream stream("test.dat");
    BinaryReader reader(stream);
    char data;
    reader.read(data);

    ASSERT_EQ(char(0x41), data);
}

誰かの時間節約に役立ちますように(未来の自分を含む)。