CMakeプロジェクトにgoogletestを導入した話

:::note info
最新の情報に合わせて新規記事を記述しました。
https://qiita.com/utkamioka/items/cacb1001bd2abf605b15 を参照してください。
:::

# はじめに

CMakeで構成されたC++のプロジェクトに、[googletest](https://github.com/google/googletest/)によるユニットテストを組み込む方法です。

公式で説明されている方法の一つ（["Incorporating Into An Existing CMake Project"](https://github.com/google/googletest/tree/master/googletest#incorporating-into-an-existing-cmake-project)）を改めて解説し直すだけですが、C++もCMakeも知らないエンジニアが、なぜかマネージャーとしてC++のプロジェクトをレビューせざるを得なくなった場合に、せめてテスト環境だけでも整えるため、最小限の手順でユニットテストに対応するための覚書として残しておきます。

## この方法のメリット

1. OSが持つgoogletestライブラリに依存しないので、
  * （yumやaptのための）管理者権限が不要です。
  * 誰が、どこで（どのOSで）実行しても、安定した結果が得られます。
2. ビルドの実行ごとにgoogletestのソースコードをcloneするので、
  * 最新のgoogletestを利用できます（バージョンを指定することも出来ます）。
  * ソースリポジトリを汚さず、必要最小限のものだけでプロジェクトを構成できます。

# 手順

## 手順0: 普通のCMakeプロジェクトを作成

とりあえず既にあるという前提です。
よく知りませんが、イマドキ（？）のC++のビルドツールはCMakeで良いんですよね？それとも、もっと良いものがあるんでしょうか？

```sh
$ tree
.
├── CMakeLists.txt
└── src
    ├── factorial.cpp
    ├── factorial.h
    └── main.cpp
```

```CMake:CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(example_googletest_on_cmake)

set(CMAKE_CXX_STANDARD 14)

add_executable(main src/main.cpp src/factorial.cpp)
```

今回紹介する方法は「CMakeのバージョンが2.8.2以上」ですので、それ以上であれば`cmake_minimum_required()`は何でも良いはずです。

`main.cpp`や`factorial.{cpp,h}`の内容は省きます。ファイル名から適当にお察しください。
今回は`factorial.cpp`がテストしたいファイルになります。

## 手順1: `CMakeLists.txt.in`を作成

`CMakeLists.txt`と同じディレクトリに`CMakeLists.txt.in`を作成します。
内容は https://github.com/google/googletest/tree/master/googletest#incorporating-into-an-existing-cmake-project と同じで構いません。

```cmake:CMakeLists.txt.in
cmake_minimum_required(VERSION 2.8.2)

project(googletest-download NONE)

include(ExternalProject)
ExternalProject_Add(googletest
  GIT_REPOSITORY    https://github.com/google/googletest.git
  GIT_TAG           main
  SOURCE_DIR        "${CMAKE_CURRENT_BINARY_DIR}/googletest-src"
  BINARY_DIR        "${CMAKE_CURRENT_BINARY_DIR}/googletest-build"
  CONFIGURE_COMMAND ""
  BUILD_COMMAND     ""
  INSTALL_COMMAND   ""
  TEST_COMMAND      ""
)
```

リンクするgoogletestのバージョンを固定したい場合は、`GIT_TAG`にタグやコミットIDを指定してください。

## 手順2: 既存の`CMakeLists.txt`に設定を追加

既存の`CMakeLists.txt`に、手順1で作成した`CMakeLists.txt.in`を読み込んだり、諸々の設定を追加します。
基本的には https://github.com/google/googletest/tree/master/googletest#incorporating-into-an-existing-cmake-project に記載された以下の内容を追記するだけです。

ただし、最後の数行は既存プロジェクトに合わせた書き換えが必要になります。詳しくは次の手順で説明します。

```cmake:CMakeLists.txt
# Download and unpack googletest at configure time
configure_file(CMakeLists.txt.in googletest-download/CMakeLists.txt)
execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" .
  RESULT_VARIABLE result
  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/googletest-download )
if(result)
  message(FATAL_ERROR "CMake step for googletest failed: ${result}")
endif()
execute_process(COMMAND ${CMAKE_COMMAND} --build .
  RESULT_VARIABLE result
  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/googletest-download )
if(result)
  message(FATAL_ERROR "Build step for googletest failed: ${result}")
endif()

# Prevent overriding the parent project's compiler/linker
# settings on Windows
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)

# Add googletest directly to our build. This defines
# the gtest and gtest_main targets.
add_subdirectory(${CMAKE_CURRENT_BINARY_DIR}/googletest-src
                 ${CMAKE_CURRENT_BINARY_DIR}/googletest-build
                 EXCLUDE_FROM_ALL)

# The gtest/gtest_main targets carry header search path
# dependencies automatically when using CMake 2.8.11 or
# later. Otherwise we have to add them here ourselves.
if (CMAKE_VERSION VERSION_LESS 2.8.11)
  include_directories("${gtest_SOURCE_DIR}/include")
endif()

# Now simply link against gtest or gtest_main as needed. Eg
add_executable(example example.cpp)
target_link_libraries(example gtest_main)
add_test(NAME example_test COMMAND example)
```

## 手順3: `CMakeLists.txt`をプロジェクトに合わせて書き換え

手順2で追記したgoogletestの設定内容を、現在のプロジェクトに合わせて書き換えます。
書き換えが必要なのは最後の3行だけです。

```cmake:CMakeLists.txt(書き換え前)
# Now simply link against gtest or gtest_main as needed. Eg
add_executable(example example.cpp)
target_link_libraries(example gtest_main)
add_test(NAME example_test COMMAND example)
```

```cmake:CMakeLists.txt(書き換え後の例)
add_executable(${PROJECT_NAME}-googletest src/factorial.cpp test/test_factorial.cpp)
target_link_libraries(${PROJECT_NAME}-googletest gtest_main)
target_include_directories(${PROJECT_NAME}-googletest PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)

add_test(NAME test COMMAND ${PROJECT_NAME}-googletest)
enable_testing()
```

### `add_executable()`

* https://cmake.org/cmake/help/latest/command/add_executable.html
* 生成するgoogletestの実行ファイル（`${PROJECT_NAME}-googletest`）と、
  リンクするファイル（テストコードとテスト対象のコードを含むファイル）を列挙します。
* 実行ファイル名は変更せず`example`のままでも大丈夫ですが、、、まぁ普通に`example`は無いですよね。
* テストコードはtestディレクトリに配置（`test/test_factorial.cpp`）するようにしています。
* `main()`関数はgoogletestに含まれる方を使いますので、
  実行ファイルをビルドするようなプロジェクトの場合、`main()`を含むファイルは除外してください。
* 言うまでもありませんが、
  実際のプロジェクトでは大量のファイル名を何度も記述することになるので、変数に格納して使い回すのが定石です。

### `target_link_libraries()`

* https://cmake.org/cmake/help/latest/command/target_link_libraries.html
* googletestの`main()`を含むライブラリ`gtest_main`をリンクします。
* ターゲットファイル名は`add_executable()`の記述に合わせて書き換えてください。

### `target_include_directories()`

* https://cmake.org/cmake/help/latest/command/target_include_directories.html
* 通常、テストコードの中でテスト対象の関数を呼び出すには、それを宣言したヘッダファイルをincludeする必要があります。
  ですので、ヘッダファイルが配置してあるディレクトリを記述してください。
* この例は、全てのヘッダファイルがsrcディレクトリに配置してあるという仮定に基づいた設定です。
  ライブラリをビルドするプロジェクトの場合、公開するAPIについては個別にincludeディレクトリなどに置くケースもあるはずです。
  その場合は適宜記述を変更してください。
* ターゲットファイル名は`add_executable()`の記述に合わせて書き換えてください。

### `add_test()`

* https://cmake.org/cmake/help/latest/command/add_test.html
* `ctest`経由で実行されるテストを記述します。
* `COMMAND`には`add_executable()`に記述したターゲットファイル名を指定してください。
* `NAME`は、複数のテストを構成する場合には、
  個別にユニークな名前を指定する必要があるそうですが、今回は1個だけなので何でも良いです。
  （ドキュメントに記述が見つかりませんでしたが、
  `add_test()`の記述が1個だけの場合はNAMEの宣言すら必要ありませんでした。）
* `ctest`を使わずコマンドラインから直接実行してもテスト可能なので、`add_test()`は無くても構いません。

### `enable_testing()`

* https://cmake.org/cmake/help/latest/command/enable_testing.html
* `ctest`でテストを実行する場合には`add_test()`と合わせて宣言します。
* 他にも、生成される`Makefile`にtestというルールを追加する機能もあるようです。

## 手順4: テストを書く＆実行する

本題（CMakeプロジェクトへのgoogletestの組み込み方法）とは別の話なので、簡単に。

### テストコード

googletestの使い方は[「Google Test ドキュメント日本語訳」](http://opencv.jp/googletestdocs/index.html)で丁寧に説明されています。
「上級ガイド」や「よくある質問」も役に立ちそうな情報がてんこ盛りなので、一通り目を通しておきましょう。

```c++:test/test_factorial.cpp
#include <stdexcept>
#include "gtest/gtest.h"

#include "factorial.h"

TEST(test_factorial, positive_values) {
    EXPECT_EQ(1, factorial(1));
    EXPECT_EQ(2, factorial(2));
    EXPECT_EQ(6, factorial(3));
}

TEST(test_factorial, zero) {
    EXPECT_EQ(1, factorial(0));
}

TEST(test_factorial, negative_value) {
    EXPECT_THROW(factorial(-1), std::invalid_argument);
}
```

脱線になりますが、googletestには`what()`の内容をテストできるAssertionは無いのでしょうか？try-catchしないとダメ？

### ビルド

普通にCMakeのビルドと同じです。

```sh
$ mkdir build
$ cd build
$ cmake ..
$ make all
```

### テスト実行

`add_executable()`に記述したターゲットファイル名を実行するだけです。
`add_test()`と`enable_testing()`を記述してある場合は、`ctest`や`make test`でも実行可能です。
[「テストプログラムを実行する：高度なオプション」](http://opencv.jp/googletestdocs/advancedguide.html#adv-running-test-programs-advanced-options)も参考にしてください。

```sh
$ ./example_googletest_on_cmake-googletest
Running main() from /any/where/build/googletest-src/googletest/src/gtest_main.cc
[==========] Running 3 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 3 tests from test_factorial
[ RUN      ] test_factorial.positive_values
[       OK ] test_factorial.positive_values (0 ms)
[ RUN      ] test_factorial.zero
[       OK ] test_factorial.zero (0 ms)
[ RUN      ] test_factorial.negative_value
[       OK ] test_factorial.negative_value (1 ms)
[----------] 3 tests from test_factorial (7 ms total)

[----------] Global test environment tear-down
[==========] 3 tests from 1 test suite ran. (14 ms total)
[  PASSED  ] 3 tests.
```

# おわりに

「なんか臭い」けど「C++の知識も経験もない」ので「ここ本当に正しい？」と消極的な指摘しか出来ない状態でしたが、ユニットテストを素早く回せる環境があることで「こういうふうに使うと期待した動作にならないので確認してね（ニッコリ）」と具体的なコードで殴り合い^H^H^H^H対話ができるようになりました。
手に負えそうにない技術をマネジメントする場合には、まず真っ先に、使い物になるユニットテスト環境を整備し、ユニットテストをとっかかりにエンジニア目線で参加するのがオススメです。