みつきんのメモ

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

CMake C++でユニットテスト入門(初級編)

はじめに

C++でコードを書く時にユニットテストも書きたい。

CMakeにはテストを実行するための仕組みがある。

CTest

CTestはいわゆるテストランナーで、CMakeがサポートするテストランナーの中では一番シンプルなもの。 指定されたものを実行するだけのシンプルなもの。(CMakeに他にテストランナーはなさそう)

CTestに関してはCMake: CTestが詳しい。

テストプログラム

簡単なテストプログラムを作成して実際に動かしてみる。

まずはCTestも何も使用しない単純なプロジェクトを作る。

ファイルの構成は次のようなもの。

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

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(hello)

add_executable(${PROJECT_NAME} main.cpp hello.cpp)

hello.h

#ifndef HELLO_H
#define HELLO_H

#include <string>

class Hello {
public:
    std::string hello(const char* const p = "") const;
};

#endif //HELLO_H

hello.cpp

#include "hello.h"
#include <sstream>
#include <stdexcept>

std::string Hello::hello(const char *const p) const {
    if (p == nullptr)
        throw std::runtime_error("This method can not accept the nullptr.");
    if (*p == '\0')
        return "empty";
    std::stringstream ss;
    ss << "Hello " << p;
    return ss.str();
}

main.cpp

#include "hello.h"
#include <iostream>
#include <cassert>

int main() {
    Hello h;
    //assert(h.hello(nullptr) == ""); //This will be error, therefore commented out.
    assert(h.hello() == "empty");
    assert(h.hello("") == "empty");
    assert(h.hello("John Doe") == "Hello John Doe");
    std::cout << "done" << std::endl;
    return 0;
}

ビルドと実行

下記のコマンドでビルドする。

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

実行してみる。

$ ./hello 
done

問題なく終了する。

CTestを使ってみる

CMakeLists.txt

下記のように修正する。

cmake_minimum_required(VERSION 3.10)
project(hello)

add_executable(${PROJECT_NAME} main.cpp hello.cpp)

# Enable the testing features.
enable_testing()

# Add the test which is simply run the application.
add_test(NAME run_test COMMAND ${PROJECT_NAME})

enable_testing()でテスト機能を有効化して、CTestで実行可能なadd_testでテストを定義する。

CTestは単純なランナーなので、実はどんな実行ファイルでも指定可能となっている。 ここではサンプルプロジェクトとして作ったhelloが実行されるように指定した。

テストの実行

CTestで追加されたテストはctestコマンドやmake testで実行可能となっている。

下記はctestで実行した例。

$ ctest
Test project /home/mickey/work/c_lang/ctest/build
    Start 1: run_test
1/1 Test #1: run_test .........................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.01 sec

100% tests passedとなっており、成功したということになっている。

成功/失敗の判定

前述の通りテストは成功したことになっている。CTestではどんな実行ファイルでも指定できると前述したが、 成功/失敗の判断はどの様にしているのか。 これは単純にプログラム(main関数)からの戻り値が0かどうかだけで判断している。

プログラムが異常終了した場合やmainから0以外が返されると失敗になる。

試しにmain.cppの戻り値を0にして実行してみる。

diff --git a/main.cpp b/main.cpp
index 2d870a8..1cc8fe7 100644
--- a/main.cpp
+++ b/main.cpp
@@ -9,5 +9,5 @@ int main() {
     assert(h.hello("") == "empty");
     assert(h.hello("John Doe") == "Hello John Doe");
     std::cout << "done" << std::endl;
-    return 0;
+    return 1;
 }

実行結果は下記の通り。

$ ctest
Test project /home/mickey/work/c_lang/ctest/build
    Start 1: run_test
1/1 Test #1: run_test .........................***Failed    0.00 sec

0% tests passed, 1 tests failed out of 1

Total Test time (real) =   0.01 sec

The following tests FAILED:
      1 - run_test (Failed)
Errors while running CTest

0% tests passedとなって、きちんと失敗することが確認できた。

実用的なテスト

さすがにassertを挟んだだけの、しかもprojectのメインプログラムを指定するというのは、実用的な例とは言えないので、 メインプログラムとは別にきちんとしたユニットテストを追加してみる。

acutest

今回はヘッダファイルのみで構成されており、導入が簡単なacutestを使ってみる。

導入方法はヘッダファイルをダウンロードして、テストのソースコードからインクルードするだけ。

$ wget https://raw.githubusercontent.com/mity/acutest/master/include/acutest.h

再配布について

acutestの再配布の扱いについては、ヘッダに必要なことは全部記述してあるので、そのまま使う分にはヘッダーを追加するだけで問題ないとのこと。

Q: Do I need to distribute file README.md and/or LICENSE.md?

A: No. The header acutest.h includes URL to our repo, copyright note and the MIT license terms inside of it. As long as you leave those intact, we are completely fine if you only add the header into your project. After all, the simple use and all-in-one-header nature of it is our primary aim.

テストの追加

test_hello.cppとして下記の内容で作成する。

#include "acutest.h"
#include "hello.h"
#include <stdexcept>

void test_hello() {
    Hello h;
    TEST_EXCEPTION(h.hello(nullptr), std::runtime_error);
    TEST_ASSERT(h.hello() == "empty");
    TEST_ASSERT(h.hello("") == "empty");
    TEST_ASSERT(h.hello("John Doe") == "Hello John Doe");
}

TEST_LIST = {
   { "hello", test_hello },
   { nullptr, nullptr }
};

main関数はacutest側で用意してくれる。

テスト用のソースコードが複数になるようなケースでは下記のようにする。

#define TEST_NO_MAIN
#include "acutest.h"

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(hello)

add_executable(${PROJECT_NAME} main.cpp hello.cpp)

# Enable the testing features.
enable_testing()

# Add the executable for the acutest case.
add_executable(test_hello test_hello.cpp hello.cpp)

# Add the test which is run the acutest case.
add_test(NAME run_test COMMAND test_hello)

テスト用の実行ファイルを定義して、add_testでそのコマンドを実行するように定義し直す。

テストの実行

ctestを実行する。

$ mkdir build && cd build
$ cmake ..
$ make -j $(nproc)
$ ctest
Test project /home/mickey/work/c_lang/ctest/build
    Start 1: run_test
1/1 Test #1: run_test .........................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.01 sec

100% tests passedとなり、テストは成功している。

まとめ

CMakeはCTestというテストランナーを持っている。単純なランナーであるため使用するテストフレームワークは限定されない。 もっというと実行されるテスト用の実行ファイルはテストフレームワークで作成されたものである必要もない。

さまざまなテストフレームワークが利用可能という点でこれは利点ではあるが、 逆にいうとテストが失敗した際にどのテストが失敗したかなどの、テスト結果に対する表示がおおざっぱだというデメリットもある。

CMakeはGoogleTestに特化したテストランナーの機能GoogleTestをCTestと組み合わせて使用するための機能も持っているので、GoogleTestを使用する場合にはそちらを使用すると良さそう。