はじめに
以前書いた記事で、IoT機器のハブになるアプリケーションを公開した。
公にしているということは人様に使われる可能性があるわけで、それであればちゃんとテストコードを書こうと思いテストツールを導入することにした。
採用したツールはGoolge Testと呼ばれるGoogel謹製のC++用の単体テストツールである。
理由は単純で、仕事でも使っているので馴染みがあったからである。
この記事ではCMakeを使ってGoogle Testをプロジェクトに導入する方法ついてメモした内容を紹介する。
Google Testの導入
Google Testを組み込むにはCMakeを使うと簡単そうなので、ビルドチェーンをMakefileからCMakeに書き換えた
フォルダ構成は以下のようになっている
/
├ src/
│ ├ CMakeLists.txt
│ └ (テスト対象となるソースコード)
├ test/
│ ├ CMakeLists.txt
│ ├ mocks/
│ │ ├ CMakeLists.txt
│ │ └ (モックファル)
│ └ unit-tests/
│ ├ CMakeLists.txt
│ └ (ユニットテストコード)
└ CMakeLists.txt
- テスト対象となるコードをsrc/ディレクトリ以下に入れ、テスト関連のファイルをtest/ディレクトリ以下に配置している
- test/ディレクトリはさらにmocks/とunit-tests/ディレクトリに分かれている
- Google Mockという単体テスト用にモッククラスを作れるツールがあるのだが、それを使ったモッククラスのファイルをmocks/ディレクトリ以下に入れている
- test/ディレクトリはGoogle Testを使って書かれたテストコードが入っている
tests/ディレクトリ直下にあるCMakeLists.txtに以下を追加し、GoogleTestを使えるようにする
cmake_minimum_required(VERSION 3.14)
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
enable_testing()
set(GTEST_INCLUDE_PATH /usr/local/include/gtest)
set(GMOCK_INCLUDE_PATH /usr/local/include/gmock)
またカバレッジ計測のために以下も追加する
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
find_program(LCOV_EXECUTABLE lcov)
find_program(GENHTML_EXECUTABLE genhtml)
add_custom_target(coverage
DEPENDS test
COMMAND ${LCOV_EXECUTABLE} -d . -c -o coverage.info
COMMAND ${LCOV_EXECUTABLE} -r coverage.info "*/googletest/*" "*/googlemock/*" "*/tests/*" "*/c++/*" -o coverageFiltered.info
COMMAND ${GENHTML_EXECUTABLE} -o lcovHtml --num-spaces 4 -s --legend coverageFiltered.info
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
-
COMMAND ${LCOV_EXECUTABLE} -r coverage.info "*/googletest/*" "*/tests/*" "*/c++/*" -o coverageFiltered.infoこの行は指定した文字列にマッチしたファイルをカバレッジ対象から除外してcoverageFiltered.infoという新しいカバレッジ情報ファイルを作成している- これがないとテスト対象でないファイルやテストコード自身までカバレッジの中身に入ってしまう
テストコードが置いてあるディレクトリと同じ場所に以下の内容を追加したCMakeList.txtを作成する
- この例ではビルドするとutという名前のバイナリができるので、それを実行するとテストが走る
- もし後述するモッククラスが必要な場合target_link_librariesに追加する
find_package(Poco REQUIRED Foundation JSON Util)
set(TEST_SOURCES
<test code>.cpp
)
file(GLOB TAEGET_SOURCE
${PROJECT_SOURCE_DIR}/src/<target code>.cpp
)
add_executable(ut ${TEST_SOURCES})
target_sources(ut
PRIVATE
${TAEGET_SOURCE}
)
target_include_directories(ut_mqtt
PUBLIC
${GTEST_INCLUDE_PATH}
${GMOCK_INCLUDE_PATH}
)
target_link_libraries(ut
PRIVATE
gtest
gtest_main
)
target_link_libraries(ut
PRIVATE
<Own mock>
)
set_target_properties(ut PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
テストコードを書く
テストコードはシンプルに書けば以下のような構成になる。
- TESTマクロにテストケース名とテスト名を書く
- テスト名はユニークである必要がある
- TESTの中でテスト対象のコードの関数を呼び出して何か機能を実行する
- 機能を実行した結果として特定の値が返ってくる場合、EXPECT_EQやEXPECT_STREQを使って実行結果と期待値が等しいかどうかをチェックする
- もし値が等しくない場合はテストが失敗したものとして処理される
#include <gtest/gtest.h>
#include <gtest/internal/gtest-port.h>
TEST(TestCaseName, TestName)
{
// Call target function
uint8_t hoge = some_function();
EXPECT_EQ(hoge, 1U);
}
モッククラスを書く
テストコードを書いていると、外部のモジュールから関数を呼び出したり、またその関数がテストシナリオに沿った値を返すようにしたいことがある
そのような場合にGoogle Mockを使うと外部モジュールの関数の中身をテスト用に差し替えることができる
もし外部モジュールが抽象クラスを持っているなら特別に難しいことはなく、そのクラスを継承してモッククラスを作成すれば良い
例えばIIotEventManagerという名前のクラスがあったとする
このクラスは抽象クラスなのでいくつかの純粋仮装関数を持っている
class IIotEventManager
{
public:
IIotEventManager() = default;
virtual ~IIotEventManager() = default;
virtual void onEvent(const std::string& eventName, const std::string& eventData) = 0;
virtual void registerEventHandler(const std::string& eventName, std::function<void(const std::string&)> handler) = 0;
virtual void unregisterEventHandler(const std::string& eventName) = 0;
};
このクラスのモックを作るには以下のようにしてIIotEventManagerを継承したクラスを作ればよい
各メソッドはMOCK_METHODというマクロに戻り値、メソッド名、引数を渡してやる必要がある
#include "IIotEventManager.h"
#include <gmock/gmock.h>
class IotEventManagerMock : public IIotEventManager
{
public:
IotEventManagerMock() = default;
~IotEventManagerMock() override = default;
MOCK_METHOD(void, onEvent, (const std::string& eventName, const std::string& eventData), (override));
MOCK_METHOD(void, registerEventHandler,
(const std::string& eventName, std::function<void(const std::string&)> handler), (override));
MOCK_METHOD(void, unregisterEventHandler, (const std::string& eventName), (override));
};
このモックを使う際は、テストコード内でインスタンスを生成し、EXPECT_CALLでどの関数がどのように呼ばれて欲しいかを定義すればいい
- この例ではmqttManagerはshared_ptr<IIotEventManager>を受け取り、その後IIotEventManager内のonEventを呼び出す
-
EXPECT_CALL(*eventManagerMock, onEvent(testing::_, testing::_)).Times(1);はこのインスタンスのonEventが一回だけ呼ばれたら期待動作だと判定するという意味である- 引数の(testing::, testing::)はどんな値が入ってもいいという意味である
- EXPECT_CALLはそのメソッドが呼び出される前に定義しておく必要がある
auto eventManagerMock = std::make_shared<IotEventManagerMock>();
mqttManager.setMediator(eventManagerMock);
EXPECT_CALL(*eventManagerMock, onEvent(testing::_, testing::_)).Times(1);
:
//call some function which will calls onEvent()
mqttManager.onMessageReceived(&msg);
問題は抽象クラスをもたない、作れないモジュールをモックしたい場合である
その場合各関数に対応するメソッドを持つモッククラスを作り、そのモッククラスのメソッドを呼び出すような実装を新たに作る、といった方法が使える
例えば、MQTTのライブラリはC言語で書かれているので抽象クラスを持っていない
そのため、以下のようMosquittoMockクラスとそれを呼び出す各関数の実装を作成する
#include <mosquitto.h>
#include <gmock/gmock.h>
class MosquittoMock;
extern MosquittoMock* gMosqMock;
class MosquittoMock
{
public:
MosquittoMock() {gMosqMock = this;};
~MosquittoMock() {gMosqMock = nullptr;};
MOCK_METHOD(int, mosquitto_lib_init, ()), (override);
:
};
#include "MosquittoMock.h"
MosquittoMock* gMosqMock = nullptr;
extern "C" {
int mosquitto_lib_init(void) {
return gMosqMock ? gMosqMock->mosquitto_lib_init() : MOSQ_ERR_SUCCESS;
}
:
}
こうして作ったモックをライブラリとして作成する
MosquittoMock.cppという名前で上述のモックコードを作成し、以下のようにCMakeLists.txtに追加してMosquittoMockという名前のライブラリが作られるようにする
add_library(MosquittoMock STATIC
MosquittoMock.cpp
)
target_include_directories(MosquittoMock PUBLIC
${GTEST_INCLUDE_PATH}
${GMOCK_INCLUDE_PATH}
${CMAKE_SOURCE_DIR}/tests/mocks
/usr/include/
)
テストコードをビルドするCMakeLists.txtには以下のようにMosquittoMockライブラリをロードするようにする
target_link_libraries(ut_mqtt
PRIVATE
${DBUS_LIBRARIES}
MosquittoMock
)
テストコード内では以下のようにモッククラスを作り、mqttManager.initが呼ばれたら一回だけMOSQ_ERR_SUCCESSを返すように定義する
MosquittoMock mosqMock;
EXPECT_CALL(mosqMock, mosquitto_lib_init())
.WillOnce(testing::Return(MOSQ_ERR_SUCCESS));
:
mqttManager.init("iot_device_hub");
実行結果
テストバイナリを実行するとテストが走り、成功すると以下のような表示が出る
もし失敗した場合はその理由が表示される
test/ディレクトリで定義したcoverageターゲットを実行すると以下のようなHTMLファイルが生成されるので、現在のカバレッジを確認することができる

参考
