みつきんのメモ

組み込みエンジニアです。Interface誌で「Yocto Projectではじめる 組み込みLinux開発入門」連載中

CMakeでGoogleTest(gtest_add_tests vs gtest_discover_tests)

はじめに

CMakeにはCTestというテストランナーがある。

CTestはテスト用の実行ファイル(テストバイナリ)が1つのテストとして認識される。

GoogleTest(GTest)のようなテストフレームワークの場合、1つのテストバイナリに複数のテストケースが含まれることが常となる。

そこでCMakeにはGTestのテストが一つのCTestとして扱われるようにするためのサポート機能がある。

テスト対象のプログラム

CMake C++でユニットテスト入門(初級編)で作成した、なんの役にも立たないテストプログラムを使用する。

使用するのは下記のファイル。

  • hello.cpp
  • hello.h

GTest

aptでインストール

ExternalProjectとかいろいろあるが、まずそれ以外のことを試したいので、googletestのパッケージをaptでインストールする。

$ sudo apt install -y googletest

ちなみに作業環境はUbuntu 20.04

テスト

このサンプルプログラムに対するGoogleTestを以下のように作ってみる。

#include "gtest/gtest.h"
#include "hello.h"
#include <stdexcept>

Hello h;

/* This block will uncomment after.
TEST(HelloTest, NullPtr) {
    EXPECT_THROW(h.hello(nullptr), std::runtime_error);
}
*/

TEST(HelloTest, default_param) {
    EXPECT_EQ(h.hello(), "empty");    
}

TEST(HellsoTest, empty_string) {
    EXPECT_EQ(h.hello(""),  "empty");
}

TEST(HelloTest, normal_case) {
    EXPECT_EQ(h.hello("John Doe"), "Hello John Doe");
}

NullPtrのテストはあえてコメントアウトしておく。

CMakeLists.txt

まずは、テストランナーを使用せずに、GTestをリンクしたテストバイナリを生成する。

cmake_minimum_required(VERSION 3.10)

# To use the googletest
find_package(GTest REQUIRED)

# Add the executable for the testcase which is using googletest
add_executable(test_hello test_hello.cpp hello.cpp)
target_link_libraries(test_hello GTest::GTest GTest::Main)

テストバイナリのビルド

この時点でディレクトリ構成は次のようになっている。

.
├── CMakeLists.txt
├── hello.cpp
├── hello.h
└── test_hello.cpp

次のようにしてビルドする。

$ mkdir build && cd build
$ cmake ..
# make -j $(nproc)

テストバイナリの実行

$ ./test_hello 
Running main() from /home/mickey/work/trash/googletest-release-1.10.0/googletest/src/gtest_main.cc
[==========] Running 3 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 3 tests from HelloTest
[ RUN      ] HelloTest.default_param
[       OK ] HelloTest.default_param (0 ms)
[ RUN      ] HelloTest.empty_string
[       OK ] HelloTest.empty_string (0 ms)
[ RUN      ] HelloTest.normal_case
[       OK ] HelloTest.normal_case (0 ms)
[----------] 3 tests from HelloTest (0 ms total)

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

3つのテストがPASSしている。

ここまでで下準備完了。

gtest_add_tests

gtest_add_testsを試す。この機能はCMake 3.1の時点で追加されている。

今の形になったのは3.9の頃らしい。

CMakeLists.txtの修正

次のようにしてgtest_add_testsを使用してみる。

cmake_minimum_required(VERSION 3.10)
project(hello)

# Enable the testing features.
enable_testing()

# To use the googletest
find_package(GTest REQUIRED)

# Enable the GoogleTest integration.
include(GoogleTest)

# Add the executable for the testcase which is using googletest
add_executable(test_hello test_hello.cpp hello.cpp)
target_link_libraries(test_hello GTest::GTest GTest::Main)

# Add the test case use the old feature.
gtest_add_tests(TARGET test_hello)

ctestの実行

ctestでテストを実行してみる。

$ ctest
Test project /home/mickey/work/c_lang/gtest/build
    Start 1: HelloTest.NullPtr
1/4 Test #1: HelloTest.NullPtr ................   Passed    0.00 sec
    Start 2: HelloTest.default_param
2/4 Test #2: HelloTest.default_param ..........   Passed    0.00 sec
    Start 3: HelloTest.empty_string
3/4 Test #3: HelloTest.empty_string ...........   Passed    0.00 sec
    Start 4: HelloTest.normal_case
4/4 Test #4: HelloTest.normal_case ............   Passed    0.00 sec

100% tests passed, 0 tests failed out of 4

Total Test time (real) =   0.01 sec

コメントアウトされているはずのNullPtrテストが実行されている。

gtest_add_testsの弱点

一つのテストバイナリを指定すると、GTestのテストケースに対応してGTestを実行してくれるが、下記の問題点をはらんでいる。

  1. テスト抽出のタイミングがcmake実行時
  2. 文字列ベースでテストを抽出するため、コメントアウトされていることを区別しない

つまり、Cコンパイラは当然のように知っているコメント行や#if 0などは感知されないため、TESTマクロの行を愚直に検出してしまう。

また、テストの検出がcmake実行時であるため、テストケースを追加したり削除したりする場合、cmakeから実行し直す必要がある。

gtest_discover_tests

gtest_discover_testsを試す。この機能は3.10で追加されている

CMakeLists.txtの修正

次のようにしてgtest_discover_testsを使用してみる。

cmake_minimum_required(VERSION 3.10)
project(hello)

# Enable the testing features.
enable_testing()

# To use the googletest
find_package(GTest REQUIRED)

# Enable the GoogleTest integration.
include(GoogleTest)

# Add the executable for the testcase which is using googletest
add_executable(test_hello test_hello.cpp hello.cpp)
target_link_libraries(test_hello GTest::GTest GTest::Main)

# Add the test case use the gtest feature.
gtest_discover_tests(test_hello)

ctestの実行

ctestでテストを実行してみる。

$ ctest
Test project /home/mickey/work/c_lang/gtest/build
    Start 1: HelloTest.default_param
1/3 Test #1: HelloTest.default_param ..........   Passed    0.00 sec
    Start 2: HelloTest.empty_string
2/3 Test #2: HelloTest.empty_string ...........   Passed    0.00 sec
    Start 3: HelloTest.normal_case
3/3 Test #3: HelloTest.normal_case ............   Passed    0.00 sec

100% tests passed, 0 tests failed out of 3

Total Test time (real) =   0.01 sec

コメントアウトされているNullPtrテストは実行されていない。

NullPtrコメントの削除

NullPtrのテストをアンコメントして実行してみる。

$ make
$ ctest
Test project /home/mickey/work/c_lang/gtest/build
    Start 1: HelloTest.NullPtr
1/4 Test #1: HelloTest.NullPtr ................   Passed    0.00 sec
    Start 2: HelloTest.default_param
2/4 Test #2: HelloTest.default_param ..........   Passed    0.00 sec
    Start 3: HelloTest.empty_string
3/4 Test #3: HelloTest.empty_string ...........   Passed    0.00 sec
    Start 4: HelloTest.normal_case
4/4 Test #4: HelloTest.normal_case ............   Passed    0.00 sec

100% tests passed, 0 tests failed out of 4

Total Test time (real) =   0.01 sec

cmakeせずに、makeでテストバイナリをビルドし直すだけでNullPtrのテストも実行されるようになった。

gtest_discover_testsの強み

  1. テスト抽出のタイミングがビルド時
  2. コンパイル時の情報をもとに抽出するため、コメントアウトされたテストなどは抽出されない

つまり、コメントや#if 0などははプリプロセッサやコンパイラのルールに従って適切に処理されるため、明示的に実行したくないテストに関して実行されてしまうことがない。

また、テストの抽出がビルド時であるため、テストケースを追加したり削除したりする場合、cmakeから実行する必要がない。

まとめ

CMakeにはCTestとGoogleTestをうまく強調するための機能が提供されている。

gtest_add_testsとgtest_discover_testsがあるが、 問答無用でgtest_discover_testsの方を使うべき

日本語の情報で検索するとgtest_add_testsが出てくる場合が多いので注意が必要。

参考