はじめに
突然ですが皆さん! テストコードは書いていますか!?
Siv3Dを使用したアプリケーション開発ではテストコードを書いてない人が多いのではないでしょうか?
(私も普段は書いていないのが現状です。)
小規模だったり作り切りでのアプリケーションで、そこまで保守性を気にしていないとか
そもそも何をテストしたらいいのかが、わからないとか
様々な理由があるとは思うのですが
日々の開発の中で、作った仕組みが想定通りに動いているのかが不安になったりする事もあると思います。
例えば
- ロジックの計算式が意図通りになっているか不安
- 特定の操作後に、状態は意図通りになっているか不安
- エッジケースがないか不安
などなど
そんな時にテストコードが書けると、あらかじめ想定されるパターンに対して動作確認しておくことができるので 安心感 をえることができます。
(ただし、テストコード自体が間違っていたり、想定外の不具合がおこりえるのも事実なので
信用しすぎも良くないとは思う)
今回は、テストをちゃんと書こうという説教ではなく
書きたいと思った時に、すぐに書けて安心できる状態だと良いよねというモチベーションで
OpenSiv3D開発でのテストコードの書き方の例を紹介します。
OpenSiv3Dでのテストコード
なんとOpenSiv3Dにはインストール時点で Catch2 という ユニットテストフレームワーク がThirdPartyに含まれているので直ぐにでも使用することができます。
OpenSiv3D自体のテストコードもこのCatch2で書かれているようです。
Catch2
細かい仕様は公式ドキュメントを見ていただくのが良いと思いますが
最低限の使い方だけ簡単に紹介します。
TEST_CASE
#include <ThirdParty/Catch2/catch.hpp>
TEST_CASE("TestName", "[tag1][tag2]")
{
}
まずはテストケースを追加します。
TEST_CASE
マクロにテストケースの名前と任意のタグを設定します。
タグを設定することで、テスト実行時のフィルタリングが可能です。
タグは複数指定することも出来ます。
なお、タグについては設定しなくても構いません。
- TEST_CASE("TestName", "[tag1][tag2]")
+ TEST_CASE("TestName")
REQUIRE
次にテストケースのスコープ内にテストを書いていきます。
以下はフィボナッチ数列の計算をテストする例です。
#include <ThirdParty/Catch2/catch.hpp>
// フィボナッチ数列 今回テストしたいもの
constexpr uint32 fib(uint32 x)
{
if (x == 0 || x == 1) {
return x;
}
return fib(x - 1) + fib(x - 2);
}
TEST_CASE("Fibonacci")
{
// 0
REQUIRE(fib(0) == 0);
// 1
REQUIRE(fib(1) == 1);
// 2以上
REQUIRE(fib(2) == 1);
REQUIRE(fib(10) == 55);
}
REQUIRE
マクロの引数に結果がtrue
になるように式を書いていきます。
結果がもしfalse
だった場合はテスト実行時にアサートがされます。
※ただし論理和や論理積は現在サポートされてないようなので注意
REQUIRE(true && true); // compile error ! not supported
逆に式がfalse
だったら成功になるREQUIRE_FALSE
マクロもあります。
REQUIRE_FALSE(false); // test success
SECTION
TEST_CASEとセットで使えるSECTION
マクロがあります。
TEST_CASE内にSECTIONを複数書くことができ、SECTION外のコードは各SECTION毎に毎回実行されます。
また、SECTION内にSECTIONをネストすることも可能です。
#include <ThirdParty/Catch2/catch.hpp>
TEST_CASE("Section example")
{
int32 a = 0;
REQUIRE(a == 0);
SECTION("Section 1") {
++a;
REQUIRE(a == 1);
}
SECTION("Section 2") {
a += 2;
REQUIRE(a == 2);
SECTION("Nest Section 1") {
++a;
REQUIRE(a == 3);
}
SECTION("Nest Section 2") {
a += 2;
REQUIRE(a == 4);
}
}
REQUIRE(a > 0);
}
OpenSiv3D上からテスト実行
# include <Siv3D.hpp>
#define CATCH_CONFIG_RUNNER
#include <ThirdParty/Catch2/catch.hpp>
void Main()
{
// コンソールを出す必要がある
Console.open();
Catch::Session session;
// テスト後にキー入力を待つための設定
session.useConfigData({
.waitForKeypress = Catch::WaitForKeypress::BeforeExit
});
// テスト実行
session.run();
}
とりあえずテストだけ実行するなら上記のようなコードで実行結果を確認することができます。
テスト結果はコンソール画面に出力されます。
参考程度に運用例
私の場合は、テスト用に別プロジェクトを用意するのは手間に感じたので
プロジェクト内にテストも含めてしまい
普段の開発で常に最初にテスト実行をするようにしています。
# include <Siv3D.hpp>
#if USE_TEST
#define CATCH_CONFIG_RUNNER
#include <ThirdParty/Catch2/catch.hpp>
class TestRunner
{
public:
static bool Run()
{
// コンソールを出す必要がある
Console.open();
// テスト実行
bool testSuccess = Catch::Session().run() == 0;
if (!testSuccess) {
// テスト失敗時
// 失敗に気づきやすいようにキー入力を待つようにする
static_cast<void>(std::getchar());
}
return testSuccess;
}
};
#endif
void Main()
{
#if USE_TEST
// 最初に必ず実行
TestRunner::Run();
#endif
// ↓アプリケーションの実装↓
while (System::Update())
{
// something...
}
}
こうすることで開発中にビルド結果が実行されるたびに必ずテストの結果が視界に入ります。
もしバグを埋め込んでテスト失敗した時だけは気が付きやすいようにキー入力を待つようにしています。
また、例ではUSE_TEST
のプリプロセッサによる条件分岐でテスト実行を制御しています。
これは、Releaseビルドにはテストの実行処理やテストコードそのものを含めなくてよいため、VisualStudio側でソリューション構成に合わせてプリプロセッサの定義をするようにしています。
例えばDebugの時だけテストを実行したければ以下のようにします。
余談 ソリューション構成の追加
ちなみに私の場合はReleaseビルドと同等の速度で開発をしたいので、あまりDebugビルドは使っていないのですが、そのため初期からあるDebug, Releaseとは別に独自に開発用のソリューション構成を追加しています。
Visual Studioの
ビルド > 構成マネージャー > アクティブソリューション構成 > <新規作成…> から
Releaseをコピー元にして別途構成を追加
ここではDevelopという名前にしましたが、
Developビルドに対してのみデバッグ機能の使用を許可するプリプロセッサや、テスト実行をする用のプリプロセッサを定義しています。
※Debugの構成をいじる方法でも良いのですが、管理上Releaseを複製してからカスタムするほうが楽なのでそうしています。
参考
まとめ
- OpenSiv3DにはCatch2というユニットテストフレームワークが含まれている
- テストコードを書くことで安心感を得ることができる
- 書きたいときにすぐに書ける状態だと良いと思う