C++
CMake
アジャイル
ユニットテスト
googletest

GoogleTest + CMakeでC++の実践的なユニットテスト環境を構築する

背景と目的

  • googletestを導入するための情報は、既に多くの先輩方により記述されていますが、本記事では、それらに+αの情報を加え、実際の開発現場にすぐに適用できる実践レベルの内容としてまとめることを目的とします。
  • それでは、ここで想定している実践的なテスト環境とは何かと言うと
    • ①テストフレームワークのインストール方法がスクリプト化されていること
    • ②テストフレームワークのバージョンが固定されていること
    • ③テストケースを追加する度にプロジェクトファイル(Makefileとか)を編集させないこと
    • ④テストファイルとテスト対象ファイルが対になっていること(ClassA.cpp、ClassATest.cpp、など)
    • ⑤mockを実装する手段が提供されていること
    • ⑥IDEと統合してテストのデバッグができること(別途記載予定)
    • ⑦カバレッジを行単位で表示できること(別途記載予定)
    • ⑧CIでテスト結果を表示できること(別途記載予定)
  • 上記①〜④は大したことないですね、⑤はGoogleMockを使えればクリアです。レベルの低い記事になりそうで申し訳無いですが、頑張って⑧まで記述します。
  • C++でCMake使うことを前提とします。私がmakefileを生で書きたくないというのが理由です。
  • 諸事情によりLinux環境が前提です。

サンプルコード

  • 本記事は、ミニマムなサンプルコードを追っていけば目的を達成できるようになっています
    • といってもコードのほうに詳しいコメントを記述しているわけではないので、簡単に解説を下記に記述します。
  • サンプルコードはこちら

お試し環境

  • os: Ubuntu 16.04 LTS
  • tool: gcc-5.4.0, cmake-3.10.2

解説

  • 構成は下記のようになっています。
    • srcフォルダ配下がテスト対象コード、testフォルダ配下がテストコード
      • クラス単位でファイルが分かれている前提
    • mk.shを実行するとbuild/src/にアプリケーションの実行ファイル、build/test/にテストの実行ファイルが生成
├── CMakeLists.txt
├── README.md
├── mk.sh
├── src
│   ├── CMakeLists.txt
│   ├── ClassA.cpp
│   ├── ClassA.h
│   ├── ClassB.cpp
│   ├── ClassB.h
│   └── main.cpp
└── test
    ├── CMakeLists.txt
    ├── ClassATest.cpp
    ├── ClassBTest.cpp
    └── main.cpp

  • CMakeLists.txt
    • googletestを外部プロジェクトとして取り込む
      • 利用方法は他にもありますが、この方法のほうが手動でのインストールは不要になる上、どのバージョンを使っているのか明確かな、と
      • 記載時点での最新版を使う(mockがサポートされたのは1.8.0から)
      • gmock_mainは追加しない、テストコードにも自前でmain関数を記述したほうが後々融通が効いていいかも、なので
    • テストコードでもアプリケーションコード(テスト対象コード)が必要となり、共通で使うために、ここで定義しておく
      • アプリケーションはmain.cpp以外を静的ライブラリとしてビルドして、テストコードからはそのライブラリをリンクさせることで無駄なコンパイルを避ける方法が一般的なのかもしれませんが、そうすると後でカバレッジを表示させようとしたときにコードが見えなくて困ります。
cmake_minimum_required(VERSION 3.0)

#
# project settings
#
project(SIMPLE_GOOGLETEST_CMAKE_SAMPLE LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)


#
# find pthread for googletest
#
find_package(Threads REQUIRED)


#
# import googletest as an external project
#
include(ExternalProject)


externalproject_add(
  GoogleTest
  URL https://github.com/google/googletest/archive/release-1.8.1.zip
  PREFIX ${CMAKE_CURRENT_BINARY_DIR}/lib
  INSTALL_COMMAND ""
  )


externalproject_get_property(GoogleTest source_dir)
include_directories(${source_dir}/googletest/include)
include_directories(${source_dir}/googlemock/include)


externalproject_get_property(GoogleTest binary_dir)
set(GTEST_LIBRARY_PATH ${binary_dir}/googlemock/gtest/${CMAKE_FIND_LIBRARY_PREFIXES}gtest.a)
set(GTEST_LIBRARY GTest::GTest)
add_library(${GTEST_LIBRARY} UNKNOWN IMPORTED)
set_target_properties(${GTEST_LIBRARY} PROPERTIES
  IMPORTED_LOCATION ${GTEST_LIBRARY_PATH}
  IMPORTED_LINK_INTERFACE_LIBRARIES ${CMAKE_THREAD_LIBS_INIT})
add_dependencies(${GTEST_LIBRARY} GoogleTest)


set(GMOCK_LIBRARY_PATH ${binary_dir}/googlemock/${CMAKE_FIND_LIBRARY_PREFIXES}gmock.a)
set(GMOCK_LIBRARY GTest::GMock)
add_library(${GMOCK_LIBRARY} UNKNOWN IMPORTED)
set_target_properties(${GMOCK_LIBRARY} PROPERTIES
  IMPORTED_LOCATION ${GMOCK_LIBRARY_PATH}
  IMPORTED_LINK_INTERFACE_LIBRARIES ${CMAKE_THREAD_LIBS_INIT})
add_dependencies(${GMOCK_LIBRARY} GoogleTest)


#
# create common variable
#
file(GLOB MY_SRCS ${PROJECT_SOURCE_DIR}/src/*.cpp)
set(MY_INCLUDES ${PROJECT_SOURCE_DIR}/src)


#
# sub directories
#
add_subdirectory(src)
add_subdirectory(test)
  • src/CMakeLists.txt
    • ここは特に解説する箇所がございません
add_executable(${PROJECT_NAME}
  ${MY_SRCS}
  )
target_include_directories(${PROJECT_NAME} PUBLIC
  ${MY_INCLUDES}
)
  • test/CMakeLists.txt
    • srcフォルダ配下の*.cppファイルリストから、重複しないようにsrc/main.cppを削除します
    • 上記削除したものと、testフォルダ配下の.cppファイルと.hファイルを合わせてすべてコンパイルするようにしています
      • この方法により、クラス毎にテストファイルを追加していったとしてもCMakeLists.txtの編集は不要です
set(MY_SRCS_MINUS_MAIN ${MY_SRCS})
list(REMOVE_ITEM MY_SRCS_MINUS_MAIN ${PROJECT_SOURCE_DIR}/src/main.cpp)

file(GLOB MY_TEST_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
file(GLOB MY_TEST_INCLUDES ${CMAKE_CURRENT_SOURCE_DIR}/*.h)
set(MY_BINARY_NAME "UnitTestExecutor")

add_executable(${MY_BINARY_NAME}
  ${MY_TEST_SRCS}
  ${MY_SRCS_MINUS_MAIN}
  )

target_include_directories(${MY_BINARY_NAME} PUBLIC
  ${MY_INCLUDES}
  ${MY_TEST_INCLUDES}
  )

target_link_libraries(${MY_BINARY_NAME}
  GTest::GTest
  GTest::GMock
  )

add_dependencies(${MY_BINARY_NAME}
  GoogleTest
  )
  • 各ソースコードの解説は省略します、実行すると
$./mk.sh
$./build/test/UnitTestExectutor
[==========] Running 4 tests from 4 test cases.
[----------] Global test environment set-up.
[----------] 1 test from sumA1
[ RUN      ] sumA1.normal
[       OK ] sumA1.normal (0 ms)
[----------] 1 test from sumA1 (0 ms total)

[----------] 1 test from sumA2
[ RUN      ] sumA2.normal
[       OK ] sumA2.normal (0 ms)
[----------] 1 test from sumA2 (0 ms total)

[----------] 1 test from sumB1
[ RUN      ] sumB1.normal
[       OK ] sumB1.normal (0 ms)
[----------] 1 test from sumB1 (0 ms total)

[----------] 1 test from sumB2
[ RUN      ] sumB2.normal
[       OK ] sumB2.normal (0 ms)
[----------] 1 test from sumB2 (0 ms total)

[----------] Global test environment tear-down
[==========] 4 tests from 4 test cases ran. (0 ms total)
[  PASSED  ] 4 tests.

参考