Google Test勉強録 (1) CMakeでのビルド


1. Google Testとは

Google Test はC/C++用のテストフレームワークです。

現在、githubの公式リポジトリでは、

  • Google Test (gtest): 単体テストフレームワーク
  • Google Mock (gmock): モッキングフレームワーク

の2つが同梱される形で配布されていますが、今回はこれらのうちgtestの使い方を調べます。

2. 目的

本記事は、簡単な単体テストが書けるようになるまでの個人的な勉強録です。

  • 本記事では主に、Google Testを含むプロジェクトするビルドする方法や、CMakeとの連携方法に興味があります。
  • Google Test関係の文法の説明は行いません。資料としては 公式チュートリアルその日本語訳 があります。

3. 環境構築 (Google Testの準備)

以下の内容はmacOS Sierra (10.12.6) で試しています。CMakeのバージョンは3.11.0-rc1を使っていますが、より古いバージョンでも動く可能性はあります。

3.1. ビルドツール

CMake を使います。

大枠としては、下のようなプロジェクト構成にしたいです。

.
├── CMakeLists.txt
├── src // ここにソースコードを置きたい
└── test // srcのコードのテストを置きたい

それにあたり、次の2点が気になるので調べます。

  1. Google Testはどこに置けばよいか
  2. CMakeとGoogle Testをどう連携すればよいか (i.e. CMakeLists.txtをどう書くか)

2は1に依存します。つまり、Google Test自体をどこに置いたかで、CMakeがGoogle Testの依存関係をどうやって解決すればよいかが変わります。

3.2. Google Testをどこに置くか

当然テストもC++で書くので、CMakeがGoogle Testを無事見つけられる必要がありますが、どこに置くかが問題です。kaizoumanさんのブログによると、少なくとも3通りの方法があります:

  1. ビルド済みのGoogle Testをプロジェクト外部に置く
  2. Google Testごとプロジェクトに含めてしまう
  3. CMakeのExternalProjectとして扱う

3.2.1. ビルド済みのGoogle Testをプロジェクト外部に置く

ビルド済みのGoogle Testをあらかじめローカルの適当な場所に置いておく方法です。個人開発ではこれが最も楽だと思われます。

(1) ダウンロード & ビルド

$ git clone https://github.com/google/googletest.git
$ cd googletest
$ mkdir build // 適当な名前
$ cd build
$ cmake ..
$ make

gtestの静的ライブラリは build/googlemock/gtest に生成されていました。歴史的な経緯で、googlemockがgoogletestを含むようになり、その後googletestがgooglemockを含むようになったという事情があるらしいです…。

(2) 生成物を適当なところに移動

ヘッダファイルと静的ライブラリをどこか見つけやすいところに

例えば、今回はヘッダファイルを /usr/local/include/gtest、静的ライブラリを /usr/local/lib に置きます (gmockもついでにコピーしています)。

$ cp -r googlemock/include/gmock /usr/local/include/gmock
$ cp -r googletest/include/gtest /usr/local/include/gtest
$ cp build/googlemock/*.a /usr/local/lib/
$ cp build/googlemock/gtest/*.a /usr/local/lib/

ついでに、pkg-config から見つけられる場所に設定ファイルを置いておくことにしました。

$ cp build/*.pc /usr/local/lib/pkgconfig/

ちゃんと探せているか確認してみます。

$ pkg-config --modversion gtest

1.9.0

$ pkg-config --cflags gtest

-DGTEST_HAS_PTHREAD=1 -I/usr/local/include

$ pkg-config --libs gtest

-L/usr/local/lib -lgtest

良さそうです。なお、*.pcの置き場所をPKG_CONFIG_PATH という環境変数に追加しておくのでも良いようです (参考: pkg-configに対応する)。

(3) CMakeListの書き方

ここでは、CMakeからgtestを「探す」方法のみ書きます (使う方法は後述)。

(3.1) pkg-configを明示的に使う

CMakeには pkg-config用のモジュール が用意されているため、これを利用してgtestの場所を探すことができます。CMakeLists.txtに次のような設定を書きます。

CMakeLists.txt

...
find_package(PkgConfig)
pkg_search_module(GTEST REQUIRED gtest_main)
...

うまくいけば GTEST_CFLAGSGTEST_LDFLAGS などの変数群が使えるようになります。

参考:

(3.2) CMakeのFindモジュールを使う

CMakeの比較的新しいバージョンであれば、Google Testを 探すモジュール が用意されています。

CMakeLists.txt

...
find_package(GTest REQUIRED)
...

よくわかってないですが、内部的にはpkg-configを利用しているらしい (参考: CMake:How To Find Libraries) のですが、おそらくセットされる変数などがやや異なります。

3.2.2. Google Testごとプロジェクトに含めてしまう

テストも含めた状態でコードを公開したい場合、上の方法だと難しいかもしれません。そこで、Google Testをいっそ丸ごとプロジェクトに含めてしまう方法があります。例えば、こういう状態になります。

.
├── CMakeLists.txt
├── src
└── test
    └── googletest

Google Test自体はBSD3条項ライセンスなので、そこに注意すればこのままGithub等にも置けます。見た目は美しくないかもしれません。

3.2.3. CMakeのExternalProjectとして扱う

CMakeには ExternalProject という機能があり、外部プロジェクトのコードを自動でダウンロードしてきてビルドするということができます。

結論からいうと、これをいい感じに隠蔽して使いやすくしたCMakeのモジュールが公開されているので、それを使います。

Crascit/DownloadProject -- Github

使い方はリンク先のexampleがわかりやすいです。MITライセンスです。

例えばこういう感じで置くとします。

.
├── CMakeLists.txt
├── src
└── test
└── cmake
    └── DownloadProject

そして、CMakeLists.txt にこういう感じのことを書くと、cmake実行時に自動でダウンロードを行います。最終的には2の方法と似た状況になるので、

CMakeLists.txt

cmake_minimum_required(VERSION 3.2)

...

include(cmake/DownloadProject/DownloadProject.cmake)
download_project(
    PROJ googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG master
    UPDATE_DISCONNECTED 1
)

add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR})

...

参考:

4. 簡単なテストの例

上記の3.1.1.と3.1.3.の方法を使って、簡単なテストをビルドします。
ここでは、ビルドできるかだけに興味があるため、公式のサンプルにならって、階乗を計算するだけのコードを書きます。

*/src/sample1.h

#ifndef SAMPLE1_H_
#define SAMPLE1_H_

int factorial(int n);

#endif

*/src/sample1.cpp

#include <iostream>
#include "sample1.h"

// 注: nが負のときは1になる
int factorial(int n) {
  int result = 1;
  for (int i = 1; i <= n; ++i) {
    result *= i;
  }
  return result;
}

TEST(test_case_name, test_name) マクロを使ってテストを書きます。この場合
test_case_nametest_name には任意の文字列を設定できます。下の例では、factorial関数の値を EXPECT_EQ でテストしていますが、失敗するものを故意にひとつだけ混ぜてあります。

*/test/test_sample1.cpp

#include "sample1.h"
#include "gtest/gtest.h"

TEST(FactorialTest, Negative) {
  EXPECT_EQ(1, factorial(-5));
  EXPECT_EQ(1, factorial(-10));
}

TEST(FactorialTest, Zero) {
  EXPECT_EQ(1, factorial(0));
}

TEST(FactorialTest, Positive) {
  EXPECT_EQ(1, factorial(1));
  EXPECT_EQ(2, factorial(2));
  EXPECT_EQ(120, factorial(5));
}

// 失敗するべきテスト
TEST(FactorialTest, Failure) {
  EXPECT_EQ(42, factorial(0));
}

4.1. ローカルのGoogle Testを使う場合

3.2.1.のように、ローカルにあるgtestを使う場合を考えます。このような構成にしてみます。

.
├── CMakeLists.txt
├── src
│   ├── CMakeLists.txt
│   ├── sample1.cpp
│   └── sample1.h
└── test
    ├── CMakeLists.txt
    └── test_sample1.cpp

CMakelists.txt

cmake_minimum_required(VERSION 3.10)
project(Sample1)
enable_testing()
add_subdirectory(src)
add_subdirectory(test)

src/CMakelists.txt (静的ライブラリを作っています)

cmake_minimum_required(VERSION 3.10)
add_library(Sample1 STATIC sample1.cpp)

test/CMakelists.txt

cmake_minimum_required(VERSION 3.10)

find_package(GTest REQUIRED)
include(GoogleTest)

add_executable(TestSample1 test_sample1.cpp)
target_link_libraries(TestSample1 Sample1 GTest::GTest GTest::Main)
include_directories(${PROJECT_SOURCE_DIR}/src ${GTEST_INCLUDE_DIRS})

# Google Testの各テストケースごとにCTestのテストを作成する
gtest_add_tests(TARGET TestSample1)

# CTestのテストをひとつだけ作成する
#add_test(NAME AllTests COMMAND TestSample1)

動かしてみます。

$ mkdir build
$ cd build
$ cmake ..
$ make
$ make test

結果

Running tests...
Test project ${project_dir}/build
    Start 1: FactorialTest.Negative
1/4 Test #1: FactorialTest.Negative ...........   Passed    0.02 sec
    Start 2: FactorialTest.Zero
2/4 Test #2: FactorialTest.Zero ...............   Passed    0.01 sec
    Start 3: FactorialTest.Positive
3/4 Test #3: FactorialTest.Positive ...........   Passed    0.01 sec
    Start 4: FactorialTest.Failure
4/4 Test #4: FactorialTest.Failure ............***Failed    0.01 sec

75% tests passed, 1 tests failed out of 4

Total Test time (real) =   0.05 sec

The following tests FAILED:
      4 - FactorialTest.Failure (Failed)
Errors while running CTest
make: *** [test] Error 8

Test #4: FactorialTest.Failureが無事 (?) 失敗しています。

テストの追加には、CMakeの GoogleTestモジュール を使っています。特に、gtest_add_tests はCMake v3.10から追加された機能で、Google Testの各テストケースごとにCTestのテストを個別に作成します (この場合は4つ作成されます)。

ちなみに、この部分を

add_test(NAME AllTests COMMAND TestSample1)

と変更すると、次のような結果になりました。

Running tests...
Test project ${project_dir}/build
    Start 1: AllTests
1/1 Test #1: AllTests .........................***Failed    0.02 sec

0% tests passed, 1 tests failed out of 1

Total Test time (real) =   0.03 sec

The following tests FAILED:
      1 - AllTests (Failed)
Errors while running CTest
make: *** [test] Error 8

確かに失敗しているっぽいのですが、どのテストケースが失敗したのかはわかりません。

4.2 Google Testをダウンロードしてきて使う場合

次に、3.2.3.のように、Google TestをダウンロードするところまでCMakeにやってもらう場合を考えます。

まず、先ほどと同じプロジェクト内のどこかに DownloadProject.cmake を含めます。

.
├── CMakeLists.txt
├── cmake
│   └── DownloadProject
│       └── (ここに https://github.com/Crascit/DownloadProject をクローンしてくる)
├── src
│   ├── CMakeLists.txt
│   ├── sample1.cpp
│   └── sample1.h
└── test
    ├── CMakeLists.txt
    └── test_sample1.cpp

test/CMakeLists.txt のみ次のように変更します。

cmake_minimum_required(VERSION 3.10)

include(${PROJECT_SOURCE_DIR}/cmake/DownloadProject/DownloadProject.cmake)
download_project(PROJ googletest
                GIT_REPOSITORY https://github.com/google/googletest.git
                GIT_TAG master
                UPDATE_DISCONNECTED 1
)

add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR})

add_executable(TestSample1 test_sample1.cpp)
target_link_libraries(TestSample1 Sample1 gtest_main)
include_directories(${PROJECT_SOURCE_DIR}/src)

include(GoogleTest)
gtest_add_tests(TARGET TestSample1)

動かしてみます(上の例と同じ)。

$ mkdir build
$ cd build
$ cmake ..
$ make
$ make test

cmake コマンドを打つと何やらダウンロードが始まって、build/googletest-* に色々と展開されます。make のところでGoogle Testが実際にビルドされるので多少時間がかかります。

結果:

Running tests...
Test project ${project_dir}/build
    Start 1: FactorialTest.Negative
1/4 Test #1: FactorialTest.Negative ...........   Passed    0.02 sec
    Start 2: FactorialTest.Zero
2/4 Test #2: FactorialTest.Zero ...............   Passed    0.01 sec
    Start 3: FactorialTest.Positive
3/4 Test #3: FactorialTest.Positive ...........   Passed    0.01 sec
    Start 4: FactorialTest.Failure
4/4 Test #4: FactorialTest.Failure ............***Failed    0.01 sec

75% tests passed, 1 tests failed out of 4

Total Test time (real) =   0.05 sec

The following tests FAILED:
      4 - FactorialTest.Failure (Failed)
Errors while running CTest
make: *** [test] Error 8

正しく動いているように見えます。

5. まとめ

  • gtestを使ったテストをCMakeでビルドする方法を勉強した
  • 個人的な利用ではローカルにあるものを使えば良さそう
  • gtestを含むコードを公開するときはDownloadProjectが便利そう

今回の例は下に置きました。
https://github.com/ktrmnm/gtest-learn