boost-ext/ut の使い方

しばらくブログを書けていなかったのでちょっと準備運動をする。比較的長めのものをこれから書こうと思っているからだ。


github.com

boost-ext/μtは、C++20用のテストライブラリだ。C++20らしくモジュールとしても使えるし、シングルヘッダオンリーライブラリとしても使える。

うれしいことにこれはCatch2と比べて非常にコンパイルが速く、Boost.UnitTestFrameworkと比べて非常に軽量だ。

https://github.com/boost-ext/ut#benchmarks

テストコードはラムダを使って書く。

#include <concepts>
#include <iostream>
#include <boost/ut.hpp>

template<std::unsigned_integral UInt>
constexpr UInt factorial(UInt x)
{
    return (x == 0) ? 1 : x * factorial(x-1);
}

int main()
{
    using namespace boost::ut::literals; // to use operator ""_test
    "factorial"_test = []
    {
        boost::ut::expect(factorial(0u) == 1u);
        boost::ut::expect(factorial(1u) == 1u);
        boost::ut::expect(factorial(2u) == 2u);
        boost::ut::expect(factorial(3u) == 6u);
        boost::ut::expect(factorial(4u) == 24u);
    };
    return 0;
}

この""_testは好きなところに書けるので、(コードと同じファイルにテストを書きたい、などの理由で)テストのためだけに.cppを用意したくない場合、headerであろうと書くことができる。そう、inline変数がC++20には(C++17から)あるのだ。

// factorial.hpp
namespace lib
{
template<std::unsigned_integral UInt>
constexpr UInt factorial(UInt x)
{
    return (x == 0) ? 1 : x * factorial(x-1);
}
} // lib

#ifdef ACTIVATE_UNIT_TEST
#include <boost/ut.hpp>

inline boost::ut::suite test_factorial = []
{
    using namespace boost::ut::literals; // to use ""_test
    "factorial"_test = []
    {
        boost::ut::expect(lib::factorial(0u) == 1u);
        boost::ut::expect(lib::factorial(1u) == 1u);
        boost::ut::expect(lib::factorial(2u) == 2u);
        boost::ut::expect(lib::factorial(3u) == 6u);
        boost::ut::expect(lib::factorial(4u) == 24u);
    };
};
#endif

こうしておけばmain.cppからincludeされているものはすべて、ACTIVATE_UNIT_TESTを定義している限り、テストが実行される。いやまあ、明らかにスケールしないのでファイルを分けてfactorial_test.cppみたいなものの中に書くべきだが、数ファイル以下の簡単なプロジェクトならこの程度から始めてもいいのではないか。気軽に行きましょう。

テストを有効化する際にマクロを使っているのは、せっかくmacro-freeなライブラリを使っているのに……という気持ちになるし、別にfactorial_test.cppみたいなやつ用意すればいいじゃないとも思うが、まあ。

ログを取りたいときはboost::ut::logを使う。streamっぽいインターフェースで、改行が自動的に行われる。

boost::ut::suite test_factorial = []
{
    using namespace boost::ut::literals; // to use ""_test
    "factorial"_test = []
    {
        boost::ut::log << "testing factorial() function ...";
        boost::ut::expect(lib::factorial(0u) == 1u);
        boost::ut::expect(lib::factorial(1u) == 1u);
        boost::ut::expect(lib::factorial(2u) == 2u);
        boost::ut::expect(lib::factorial(3u) == 6u);
        boost::ut::expect(lib::factorial(4u) == 24u);
    };
};

テストが落ちたときに出力する内容は、expect自体に流し込むとよい。こちらも

boost::ut::suite test_factorial = []
{
    using namespace boost::ut::literals; // to use ""_test
    "factorial"_test = []
    {
        boost::ut::log << "testing factorial() function ...";
        boost::ut::expect(lib::factorial(0u) == 1u) << "expecting 1 but got " << lib::factorial(0u);
        boost::ut::expect(lib::factorial(1u) == 1u) << "expecting 1 but got " << lib::factorial(1u);
        boost::ut::expect(lib::factorial(2u) == 2u) << "expecting 2 but got " << lib::factorial(2u);
        boost::ut::expect(lib::factorial(3u) == 6u) << "expecting 6 but got " << lib::factorial(3u);
        boost::ut::expect(lib::factorial(4u) == 24u) << "expecting 24 but got " << lib::factorial(4u);
    };
};

boost-extはほかにも様々な機能を持っているが、最低限これだけで使えないことはない。

ビルドが速いので、趣味でC++20を使えるプロジェクトでパパっとテストを書きたいときはかなりよい選択肢ではないだろうか。


boost-extはboost本体の一部ではないが、ほかにもいくつかライブラリを持っている。ちょっと見てみると面白いのではないか。