前回 CMakeのテストランナーである ctest の元で GoogleTest(gtest)フレームワークのTEST走らせました。調子に乗って今回はC++のClass のテストに手を広げてみました。前回の関数単体とはちょっと書き方が違ってました。今回は分量多いので「前回との差分」部分にフォーカスして記述いたします。
※ソフトな忘却力 投稿順 index はこちら
今となってはどーでもいい話ですが、本日は、全てのコードが失われそうな事態に直面してました。作業は、PC上のVSCodeからRaspberry Pi 3 model B+にリモート接続して行っているのですが、その肝心のラズパイの挙動が変、何度もハングりました。結局、新しいSDカードに先月末にバックアップしてあったイメージをレストアして復旧いたしました。不調になった古いSD、多分2年くらいも使っていた(電源OFFっている割合いも大だったけれど)ので良く持った方なのかも知れません。しかしバックアップ無かったらヤバかったです。肝心の今回プロジェクトのファイルはHDD側に保存していたのでこれまたOK。やはり転ばぬ先のバックアップだ、と。
C++のClassのTest Fixturesについてのドキュメント
以下のGoogleTestのドキュメントに Class を相手にするときの方法なども書かれています。特段難しいことは書かれてないみたいな気がしますが(個人の感想です)、前回の単体関数は TEST() マクロでよかったのに、今回は TEST_F()マクロにせよとか、知らないとハマりそうなことが多数です。
しかし、いつも組み込みマイコン相手でCばかりの自分、C++苦手なのに今回C++に踏み込んだのは、組込マイコン相手の単体テストはPlatformIOにまかせチャオ、という割り切りをしたためです。PlatformIOの単体テストの記事はこちら(テストフレームワークは組込には嬉しいピュアC)一方、gtest使うこちらでは、少しは苦手のC++も勉強すべし、と。
被テストクラスの定義
さて今回の被テストクラスはライブラリ化する前提であります。まずヘッダファイル部分から。
include/UnderTestClass.h
#ifndef UNDERTESTCLASS_H #define UNDERTESTCLASS_H #include <iostream> class UnderTestClass { private: int x; int y; public: UnderTestClass(); UnderTestClass(int a, int b); ~UnderTestClass(); void setX(int a) { x = a; }; void setY(int b) { y = b; }; int exec(void); }; #endif /* UNDERTESTCLASS_H */
そしてライブラリ化される本体部分
lib/UnderTestClass.cpp
#include "../include/UnderTestClass.h" UnderTestClass::UnderTestClass() { std::cout << "UnderTestClass Constructor no arg" << std::endl; } UnderTestClass::UnderTestClass(int a, int b) { x = a; y = b; std::cout << "UnderTestClass Constructor two args" << std::endl; } UnderTestClass::~UnderTestClass() { std::cout << "UnderTestClass Destructor" << std::endl; } int UnderTestClass::exec(void) { return x * y; }
上記をライブラリ化するために、lib/CMakeLists.txt に一行追加いたしました。
add_library(UnderTestClass UnderTestClass.cpp)
TESTする側
testディレクトリに上記の被テストクラスをテストするためのコードを配置しました。::testing::Test を継承するTest用のClassを定義しておくのだそうです(お名前はテキトーで良さそうだったので、TestClassとベタで行きましたが。
test/testUnderTestClass.cpp
#include <gtest/gtest.h> #include "../include/UnderTestClass.h" class TestClass : public ::testing::Test { protected: void SetUp() override { std::cout << "TextClass Setup.\n"; } void TearDown() override { std::cout << "TextClass TearDown.\n"; } }; TEST_F(TestClass, utc1) { UnderTestClass utc1; utc1.setX(2); utc1.setY(3); EXPECT_EQ(6, utc1.exec()); UnderTestClass utc2(3,4); EXPECT_EQ(11, utc2.exec()); }
なお、上記のEXPECT_EQ(11 のところは、わざとエラーを起こすための意図的誤りであります。折角なので VSCode画面でみたTestClassの様子が以下に。
さて上記のファイルの追加にともない、前回のtest/CMakeLists.txt に3か所追加しました。
まずはテスト用の実行ファイルの生成のお願い
add_executable( testUnderTestClass testUnderTestClass.cpp )
続いてテスト用の実行ファイルを生成するときにライブラリとのリンクをしていただくお願い。忘れずに gtest_main ともリンクをします。
target_link_libraries( testUnderTestClass UnderTestClass gtest_main )
最後に、テストランナー ctest がテストを見つけられるようにするお願い
gtest_discover_tests(testUnderTestClass)
プロジェクトルートのファイルの変更
プロジェクトルートにおいてある「アプリの実体」は、形だけのものなのですが、一応アップデートしてあります。こんな感じ。
main.cpp
#include <iostream> #include "include/func.h" #include "include/UnderTestClass.h" int main(int, char**) { UnderTestClass utc1; UnderTestClass utc2(5,7); std::cout << func1(1, 2) << " Hello, world!\n"; utc1.setX(3); utc1.setY(4); std::cout << "utc1: " << utc1.exec() << std::endl; std::cout << "utc2: " << utc2.exec() << std::endl; }
というわけで、ライブラリとのリンクのためにプロジェクトルートのCMakeLists.txtにも一行追加が必要です。
target_link_libraries(gtest000 func UnderTestClass)
実行結果
まずはプロジェクトルートの形ばかりのアプリを実行。
コンストラクタやデストラクタに散りばめた、殊更なメッセージがちゃんと呼ばれておるぞよ、と主張しております。
続いて、新設のクラスのテストをコンソールから走らせたものがこちら。PASSしたところは緑。FAILしたところは赤になります。何がダメなのかも単刀直入でいい感じです。こうしてFAILが分かり易いのは、 ctest 経由でTESTするよりいい感じがします(個人の感想です。)
そして、意図的に入れたバグをFIXした後に、VSCodeのステータスバーのCtestのチェックボタンを押してCtest用にビルドから実行までさせました。その様子を冒頭のアイキャッチ画像に掲げました。FAILしないときは、素っ気ないCtestのサマリの方が見やすい(多分大量にテスト項目があるとき)上に、VSCodeのステータスバーにもテスト結果が反映されます。単体テストは何か変更する度に「良からぬ副作用など起こっていないよね」と繰り返し実行する(当然PASS期待)ので、そういう時にはCtestがいいね(個人の感想です。)