はじめに
今回は、Visual Studio 2022を用いたGoogle Mockの使用方法について、簡単に説明します。
Google Mock
Mockテストとは
前回のGoogle Testで、指定関数のINに対するReturn Valueを確認することができました。
Google Mockでは、指定関数の内部で呼び出されている関数に対して、下記の様な確認をすることができます。
- 期待通りに内部関数が呼び出されているか(ex. 終了時のリソース解放関数)
- 期待通りの引数を内部関数は受け取っているか
- 内部関数の振る舞い(戻り値・取得値)により、指定関数の動作は期待通りか
ネットワーク・データベース等、別リソースを準備しなければならない関数の代返に使用すると、大変便利です。ただし、そのモックする関数は別で十分に小さなテストをクリアしておく必要があります。
Google Mockの準備
Google Mockの準備、テストプロジェクトの作成方法については、Visual Studio 2022でC++の無料Unit Test環境を構築の記事で紹介しています。こちらを参考に、用意してください。
テスト例
モック対象が仮想関数の場合
モック対象関数が仮想関数の場合、モッククラスを作成することでテストすることができます。
/// 製品コード1
class Product1 {
public:
virtual ~Product1() {}
BOOL isGreater(LONGLONG border) {
return (border > this->getTickCount()) ? TRUE : FALSE;
}
protected:
virtual LONGLONG getTickCount(void) {
return ::GetTickCount64();
}
};
/// テストコード1
class MockProduct1 : public Product1 {
public:
MOCK_METHOD(LONGLONG, getTickCount, (), ());
};
TEST(VirtualFunctionTest, isGreater) {
MockProduct1 mockP;
EXPECT_CALL(mockP, getTickCount())
.WillRepeatedly(::testing::Return(100));
EXPECT_TRUE(mockP.isGreater(101));
EXPECT_FALSE(mockP.isGreater(100));
}
モック対象が非仮想関数の場合
モック対象となる非仮想関数を一旦ラップして、テンプレートでモッククラスと製品コードクラストを切り替えるのがお勧めです。
仮想関数使用の場合と比べて、仮想テーブルを使用しないためパフォーマンス低下を心配する場合にも有効です。
/// 製品コード2
class Product2 {
public:
LONGLONG getTickCount(void) {
return ::GetTickCount64();
}
};
template <typename T>
class WrapperProduct2 {
public:
virtual ~WrapperProduct2() {}
WrapperProduct2(T* p) : _Impl(p) {};
BOOL isGreaterPerformanceHeavyVersion(LONGLONG border) {
return (border > _Impl->getTickCount()) ? TRUE : FALSE;
}
protected:
LONGLONG getTickCount(void) { return _Impl->getTickCount(); }
private:
T* _Impl;
};
/// テストコード2
class MockProduct2 {
public:
MOCK_METHOD(LONGLONG, getTickCount, (), ());
};
TEST(NonVirtualFunctionTest, isGreaterPerformanceHeavyVersion) {
MockProduct2 mockP2;
EXPECT_CALL(mockP2, getTickCount())
.WillRepeatedly(::testing::Return(100));
WrapperProduct2<MockProduct2> p2(&mockP2); // 製品コードではProduct2<WrapperProduct2>
EXPECT_TRUE(p2.isGreaterPerformanceHeavyVersion(101));
EXPECT_FALSE(p2.isGreaterPerformanceHeavyVersion(100));
}
モック対象がスタティックもしくはC形式関数の場合
前述のテンプレート案がお勧めですが、テンプレートでは記述がややこしくなりそうな場合、仮想関数でラップしてします方法もあります。
/// 製品コード3
int FreeFunctionAdd(int a, int b) {
return a + b;
}
class Utility3 {
public:
static int StaticFunctionSub(int a, int b) {
return a - b;
}
};
class Wrapper3 {
public:
//Wrapper3() {}
virtual ~Wrapper3() {};
virtual int FreeFunctionAdd(int a, int b) {
return ::FreeFunctionAdd(a, b);
}
virtual int StaticFunctionSub(int a, int b) {
return Utility3::StaticFunctionSub(a, b);
}
};
class Product3 {
public:
Product3(Wrapper3* p) : _Impl(p) {};
int HogeFunction(int x, int y) {
auto s = _Impl->StaticFunctionSub(x, y);
auto t = _Impl->FreeFunctionAdd(x, y);
return s * t;
}
private:
Wrapper3* _Impl;
};
/// テストコード3
class MockWrapper3 : public Wrapper3 {
public:
MOCK_METHOD(int, FreeFunctionAdd, (int, int), ());
MOCK_METHOD(int, StaticFunctionSub, (int, int), ());
};
TEST(FreeStaticFuntionTest, HogeFunction) {
MockWrapper3 mock3;
EXPECT_CALL(mock3, StaticFunctionSub)
.WillOnce(::testing::Return(10));
EXPECT_CALL(mock3, FreeFunctionAdd)
.WillOnce(::testing::Return(10));
Product3 p3(&mock3);
EXPECT_EQ(100, p3.HogeFunction(1, 1));
}
モック関数から値の返し方
モック関数で悩む事項の1つは、戻り値やOUTで返る引数の扱い方です。
ここでは、下記のサンプルコードを用いて、代表的な実装方法を記述します。
- 戻り値で文字列を返す例
GetCommandLineW APIを使用しています。
ReturnPointeeを用いて、返却想定の文字列のポインタを指定します。 - 戻り値で文字列配列、引数で数値を返す例
CommandLineToArgvW APIを使用しています。
引数は、SetArgPointeeで引数のn番目(0オリジン)を指定し、返却値を設定します。戻り値は、ReturnPointeeに文字列配列のポインタを設定します。
複数存在する場合は、DoAllで全体を包みます。 - 引数で文字列を返す例
GetCurrentDirectory APIを使用しています。
SetArrayArgumentで引数のn番目を指定し、返却文字列の先頭ポインタと終端NULLを含むバイトす数を設定することで、データをコピーさせます。戻り値とともにDoAllします。 - 引数で数値配列を返す例
EnumProcesses APIを使用しています。
要領は文字列の場合と同じで、返却予定値の先頭ポインタから必要バイト数をコピーさせます。
/// 製品コード4
class Win32Wrapper {
public:
virtual LPWSTR GetCommandLineW() {
return ::GetCommandLineW();
}
virtual LPWSTR* CommandLineToArgvW(LPCWSTR lpCmdLine, int* pNumArgs) {
return ::CommandLineToArgvW(lpCmdLine, pNumArgs);
}
virtual DWORD GetCurrentDirectory(DWORD nBufferLength, LPWSTR lpBuffer) {
return ::GetCurrentDirectoryW(nBufferLength, lpBuffer);
}
virtual BOOL EnumProcesses(DWORD* lpidProcess, DWORD cb, LPDWORD lpcbNeed) {
return ::EnumProcesses(lpidProcess, cb, lpcbNeed);
}
};
template<typename T>
class Product4 {
public:
Product4(T* p) : _Impl(p) {};
virtual ~Product4() { };
BOOL GetCurrentDirectorFromCmd(void) {
BOOL ret = FALSE;
LPWSTR cmdline = _Impl->GetCommandLine();
int NumArgs = 0;
LPWSTR* argv = _Impl->CommandLineToArgvW(cmdline, &NumArgs);
if ((NumArgs >= 2) && (::lstrcmpiW(argv[1], L"/get") == 0)) {
WCHAR buf[MAX_PATH + 1] = { 0 };
DWORD nBufferLength = sizeof(buf) / sizeof(WCHAR);
_Impl->GetCurrentDirectory(nBufferLength, buf);
// ...
ret = TRUE;
}
return ret;
}
BOOL existTargetProcess(DWORD target_pid) {
BOOL ret = FALSE;
DWORD pid_buffer[512] = { 0 };
DWORD cb = sizeof(pid_buffer);
DWORD cbNeeded = 0;
if (_Impl->EnumProcesses(pid_buffer, cb, &cbNeeded)) {
for (DWORD i = 0; i < cbNeeded / sizeof(DWORD); i++) {
if (target_pid == pid_buffer[i]) {
ret = TRUE;
break;
}
}
}
return ret;
}
private:
T* _Impl;
};
/// テストコード4
class MockWin32Wrapper {
public:
MOCK_METHOD(LPWSTR, GetCommandLineW, (), ());
MOCK_METHOD(LPWSTR*, CommandLineToArgvW, (LPCWSTR, int*), ());
MOCK_METHOD(DWORD, GetCurrentDirectory, (DWORD, LPWSTR), ());
MOCK_METHOD(BOOL, EnumProcesses, (DWORD*, DWORD, LPDWORD), ());
};
TEST(ReturnValueTest, OutPointerAndReturnValue) {
MockWin32Wrapper mw4a;
LPWSTR cmdval1 = L"abc";
EXPECT_CALL(mw4a, GetCommandLineW())
.WillOnce(::testing::ReturnPointee(&cmdval1));
LPWSTR argv[] = { L"hogehoge.exe", L"/get" };
int argc = sizeof(argv) / sizeof(LPWSTR);
EXPECT_CALL(mw4a, CommandLineToArgvW(::testing::_, ::testing::_))
.WillOnce(::testing::DoAll(
::testing::SetArgPointee<1>(argc),
::testing::ReturnPointee(&argv))
);
LPCWSTR curdir = L"c:\\windows\\";
EXPECT_CALL(mw4a, GetCurrentDirectory(::testing::_, ::testing::_))
.WillOnce(::testing::DoAll(
::testing::SetArrayArgument<1>(curdir, curdir + ::lstrlenW(curdir) * sizeof(WCHAR)),
::testing::Return(ERROR_SUCCESS))
);
Product4<MockWin32Wrapper> p4a(&mw4a);
EXPECT_TRUE(p4a.GetCurrentDirectorFromCmd());
MockWin32Wrapper mw4b;
DWORD pid_list[] = { 10, 20, 30 };
EXPECT_CALL(mw4b, EnumProcesses(::testing::_, ::testing::_, ::testing::_))
.WillRepeatedly(testing::DoAll(
::testing::SetArrayArgument<0>(pid_list, pid_list + sizeof(pid_list)),
::testing::SetArgPointee<2>(sizeof(pid_list)),
::testing::Return(TRUE))
);
Product4<MockWin32Wrapper> p4b(&mw4b);
EXPECT_TRUE(p4b.existTargetProcess(20));
EXPECT_FALSE(p4b.existTargetProcess(100));
}
参考
Google Mockで参考にしたURLを記載します。
- gMock for Dummies
- gMock Cookbook
- Actions Reference
- Mocking virtual functions with gMock
- Mocking non-virtual and free functions with gMock
まとめ
Google Mockでテストを楽にするためには、Google Mockの仕様・特徴を理解して、製品コードの構成を予め決めておく必要がありますが、使いこなせれば大変強力な武器になります。
テストのためのコーディングを心がけて、品質の高いコードに仕上げていきましょう。
以上、Visual StudioでGoogle Mockの紹介でした。
コメント