Edited at

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


背景と目的


  • 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
├── build.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から)

      • 1.8.0以降のgoogletestをexternal projectとして利用する方法はこちら もかなり参考にしました。

      • gmock_mainは追加しない、テストコードにも自前でmain関数を記述したほうが後々融通が効いていいかも、なので



    • テストコードでもアプリケーションコード(テスト対象コード)が必要となり、共通で使うために、ここで定義しておく


      • アプリケーションはmain.cpp以外を静的ライブラリとしてビルドして、テストコードからはそのライブラリをリンクさせることで無駄なコンパイルを避ける方法が一般的なのかもしれませんが、そうすると後でカバレッジを表示させようとしたときにコードが見えなくて困ります。

      • [update] @shohirose さんに指摘していただいたようにdeprecatedとなっていたinclude_directoriesと
        set_target_propertiesのIMPORTED_LINK_INTERFACE_LIBRARIESを修正、ついでに諸々リファクタリング





cmake_minimum_required(VERSION 3.1)

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

#
# enable external project
#
include(ExternalProject)
# set directory of external project
SET_DIRECTORY_PROPERTIES(PROPERTIES EP_PREFIX ${CMAKE_BINARY_DIR}/external)

#
# import googletest as an external project
#
externalproject_add(
googletest
URL https://github.com/google/googletest/archive/release-1.8.1.zip
UPDATE_COMMAND "" # skip update command
INSTALL_COMMAND "" # skip install step
)

externalproject_get_property(googletest source_dir)
set(GTEST_INCLUDE_PATH ${source_dir}/googletest/include)
set(GMOCK_INCLUDE_PATH ${source_dir}/googlemock/include)

externalproject_get_property(googletest binary_dir)
set(GTEST_LIBRARY_PATH ${binary_dir}/googlemock/gtest/${CMAKE_FIND_LIBRARY_PREFIXES}gtest.a) # in Unix, libgtest.a
set(GTEST_LIBRARY GTest::GTest)
add_library(${GTEST_LIBRARY} UNKNOWN IMPORTED)
set_target_properties(${GTEST_LIBRARY} PROPERTIES
IMPORTED_LOCATION ${GTEST_LIBRARY_PATH}
INTERFACE_LINK_LIBRARIES Threads::Threads)
add_dependencies(${GTEST_LIBRARY} googletest)

set(GMOCK_LIBRARY_PATH ${binary_dir}/googlemock/${CMAKE_FIND_LIBRARY_PREFIXES}gmock.a) # in Unix, libgmock.a
set(GMOCK_LIBRARY GTest::GMock)
add_library(${GMOCK_LIBRARY} UNKNOWN IMPORTED)
set_target_properties(${GMOCK_LIBRARY} PROPERTIES
IMPORTED_LOCATION ${GMOCK_LIBRARY_PATH}
INTERFACE_LINK_LIBRARIES Threads::Threads)
add_dependencies(${GMOCK_LIBRARY} googletest)

#
# create common variable
#
file(GLOB MY_SRCS ${PROJECT_SOURCE_DIR}/src/*.cpp)
set(MY_INCLUDE_PATH ${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_INCLUDE_PATH}
)


  • 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)
set(MY_TEST_INCLUDE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
set(MY_BINARY_NAME "UnitTestExecutor")

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

target_include_directories(${MY_BINARY_NAME} PUBLIC
${GTEST_INCLUDE_PATH}
${GMOCK_INCLUDE_PATH}
${MY_INCLUDE_PATH}
${MY_TEST_INCLUDE_PATH}
)

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

add_dependencies(${MY_BINARY_NAME}
googletest
)


  • 各ソースコードの解説は省略します、実行すると

$./build.sh

$./build/test/UnitTestExecutor

[==========] 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.


参考