C++と色々

主にC++やプログラムに関する記事を投稿します。

C++ Testing Framework banditの紹介

github.com

概要

banditはC++11以降を前提としたC++ユニットテストフレームワークです。
Human Friendlyらしいです。MITライセンスです。

導入

Releases · joakimkarlsson/bandit · GitHub

からzipかtar.gzでダウンロードするか git cloneして使うと良いでしょう。
私は最初CMakeList.txtがあるのでビルドする必要があるかと思ったのですが、利用するだけならヘッダーオンリーで使えるためビルドは要りませんでした。
banditはアサーションに他のライブラリを利用しており、 git submoduleで管理しています。そのためcloneする場合は

git clone --recursive https://github.com/joakimkarlsson/bandit.git

とオプションを指定してcloneする必要があります。既に普通にcloneしてしまった場合は

git submodule update --init --recursive

とすれば大丈夫です。
当然ですが、利用時はパスを通しておきます。

最初のテスト

テストに使うcppファイルそれぞれでbanditのヘッダをインクルードし、お好みで using namespaceします。以降のサンプルは全て using namespaceしています。

#include <bandit/bandit.h>
using namespace bandit;

main関数を以下のようにします:

int main(int argc, char** argv)
{
    return run(argc, argv);
}

テストをするファイルに go_banditを書きます:

go_bandit([] {
});

go_banditの中にテストコードを書いていきます。1つのcppファイルに付き1つの go_banditを記述します。

go_bandit([] {
    describe("my first test", [] {
        it("should be fail", [&] {
            AssertThat(1, Equals(2));
        });
    });
});

このようにテストを書いていきます。banditはrubyRSpecjavascriptのjasmineのようなdescrieとitを使ったBDD風テストを書けるのが特徴です。以下に完全なサンプルを示します:

#include <bandit/bandit.h>
using namespace bandit;

go_bandit([] {
    describe("my first test", [] {
        it("should fail", [&] {
            AssertThat(1, Equals(2));
        });
    });
});

int main(int argc, char** argv)
{
    return run(argc, argv);
}

実行すると次のような出力を得られます。

実行結果

F
There were failures!

my first test should fail:
main.cpp:7: Expected: equal to 2
Actual: 1


Test run complete. 1 tests run. 0 succeeded. 1 failed.

AssertThat(1, Equals(2)); の部分はbanditが利用している snowhouse という単独で動作するC++アサーションライブラリのものです。JUnit4系以降で導入されたassertThatと同じように記述できます。AssertThatは第1引数に実際のオブジェクトを受け取り、第2引数にMatcherオブジェクトを受け取ります。assertThatはテストを実行する責務を持ち、Matcherは検証に対して責務持ちます。assertThatとMatcherに分けることで利用者は自作のMatcherを作成し、検証方法を拡張することが出来ます。

主な機能(アサーション以外)

生成された実行ファイルはいくつかのオプションを受け取れます。覚えておくべきものとして:

  • --reporter=xunit xunitに対応したxmlを生成します
  • --skip=<substring> substringを含むdescribeまたはitをスキップします
  • --only=<substring> substringを含むdescribeまたはitだけ実行します

があります。

各itを実行する前に共通して事前処理がある場合は before_eachが使えます。同様に共通の事後処理がある場合は after_eachを利用できます。before_eachとafter_eachは必ずその階層と以下の階層の全てのitの前に宣言する必要があります :

#include <bandit/bandit.h>
#include <iostream>
using namespace bandit;

go_bandit([] {
    describe("describe1", [&] {
        std::cout << "describe1" << std::endl;

        before_each([&] {
            std::cout << "before_each1" << std::endl;
        });

        after_each([&] {
            std::cout << "after_each" << std::endl;
        });

        it("it1", [&] {
            std::cout << "it1" << std::endl;
        });

        it("it2", [&] {
            std::cout << "it2" << std::endl;
        });

        it("it3", [&] {
            std::cout << "it3" << std::endl;
        });
    });

    describe("describe2", [&] {
        std::cout << "describe2_1" << std::endl;

        before_each([&] {
            std::cout << "before_each2" << std::endl;
        });

        it("it4", [&] {
            std::cout << "it4" << std::endl;
        });

        describe("describe3", [&] {
            std::cout << "describe3" << std::endl;
        });

        it("it5", [&] {
            std::cout << "it5" << std::endl;
        });

        std::cout << "describe2_2" << std::endl;
    });
});

int main(int argc, char** argv)
{
    return run(argc, argv);
}

実行結果

describe1
before_each1
it1
after_each
.before_each1
it2
after_each
.before_each1
it3
after_each
.describe2_1
before_each2
it4
.describe3
before_each2
it5
.describe2_2

Success!
Test run complete. 5 tests run. 5 succeeded.

describeではなくxdescribeまたはdescribe_skipという名前で作成すると、そのdescribe以下のテストをスキップすることが出来ます。同様にitではなくxitまたはit_skipという名前で作成するとそのitをスキップすることが出来ます。

主な機能(アサーション)

正確にはbanditというよりはsnowhouseの紹介になってしまいますが、いくつかMatcherを紹介します。

  • 等価*1
// 1 + 1は2と等しい
AssertThat(1 + 1, Equals(2));
AssertThat(1 + 1, Is().EqualTo(2));

上と下の2つは全く同じです。好きな方を使って下さい。ただしIsを使った方は否定を書くことが出来ます:

// 1は2と等しくない
AssertThat(1, Is().Not().EqualTo(2));
  • 比較
// 1は2より小さい
AssertThat(1, IsLessThan(2));
AssertThat(1, Is().LessThan(2));
// 2は1より大きい
AssertThat(2, IsGreaterThan(1));
AssertThat(2, Is().GreaterThan(1));
// 1は2以上
AssertThat(1, IsLessThanOrEqualTo(2));
AssertThat(1, Is().LessThanOrEqualTo(2));
// 2は1以下
AssertThat(2, IsGreaterThanOrEqualTo(1));
AssertThat(2, Is().GreaterThanOrEqualTo(1));
  • 真偽
// 1 == 1は真である
AssertThat(1 == 1, IsTrue());
AssertThat(1 == 1, Is().True());
// 1 == 2 は偽である
AssertThat(1 == 2, IsFalse());
AssertThat(1 == 2, Is().False());
  • null
int* p = nullptr;
// pはnullptrである
AssertThat(p, IsNull());
AssertThat(p, Is().Null());

注意: Visual Studio 2015 時点で __cplusplusの値が199711Lであるため、Visual StudioではこのMatcherを使うことが出来ません。 詳しくはこちら https://github.com/joakimkarlsson/snowhouse/issues/17

  • 文字列の部分一致

文字列を検証する場合は、等値検証( Equalsまたは Is().EqualTo)を除いて、第1引数を std::string型にしておく必要があります。

// "hello world"に"world"は含まれる
auto const str = std::string{"hello world"
AssertThat(str, Contains("world"));
AssertThat(str, Is().Containing("world"));
  • 文字列の長さ
// "hello"の長さは5である
auto const str = std::string{"hello"};
AssertThat(str, HasLength(5u));
AssertThat(str, Is().OfLength(5u));
  • 文字列の開始、終端検証
auto const str = std::string{"hello world"};
// "hello world"は"hello"で始まる
AssertThat(str, StartsWith("hello"));
AssertThat(str, Is().StartingWith("hello"));
// "hello world"は"world"で終わる
AssertThat(str, EndsWith("world"));
AssertThat(str, Is().EndingWith("world"));
  • コンテナのサイズ

SLTコンテナに対するアサーションも提供されています。組み込み配列には対応していません。

auto const score = std::unordered_map<std::string, int>{{
    {"math", 90}, {"english", 80}, {"science", 70}, {"social", 75}
}};
// コンテナのサイズは4
AssertThat(score, HasLength(4));
AssertThat(score, Is().OfLength(4));

auto const empty = std::vector<int>{};
// 空のコンテナである
AssertThat(empty, IsEmpty());
AssertThat(empty, Is().Empty());
  • コンテナの要素
auto const prefectures = std::list<std::string>{
    {"千葉"}, {"滋賀"}, {"佐賀"}
};
// コンテナに"佐賀"が含まれる
AssertThat(prefectures, Contains("佐賀"));
AssertThat(prefectures, Is().Containing("佐賀"));
  • コンテナの検証

述語に該当する要素の数を検証します。ただしSTLのシーケンスコンテナの要件を満たしている必要があります。

auto const v = std::vector<std::string>{
    "Scala", "C++", "Go", "Swift"
};
// コンテナの要素が全て"#"で終わらない
AssertThat(v, Has().Not().All().EndingWith("#"));
// コンテナの要素のうち少なくとも2つは"S"から始まる
AssertThat(v, Has().AtLeast(2u).StartingWith("S"));
// コンテナの要素のうち多くても1つは"++"を含む
AssertThat(v, Has().AtMost(1u).Containing("++"));
// コンテナの要素のうち丁度2つだけ長さが5の要素がある
AssertThat(v, Has().Exactly(2u).OfLength(5u));
auto const copy = v;
// 他のコンテナと比較することもできる
AssertThat(v, EqualsContainer(copy));
AssertThat(v, Is().EqualToContainer(copy));
// 要素同士の比較に用いる述語を渡すこともできる
AssertThat(v, EqualsContainer(copy, [](auto const& l, auto const& r) { return l == r; }));
AssertThat(v, Is().EqualToContainer(copy, [](auto const& l, auto const& r) { return l == r; }));
  • 例外

例外を投げるか、投げた時の例外の型、例外オブジェクトに含まれる要素の検証ができます。

auto const empty = std::vector<int>{};
// empty.at(1)はstd::out_of_range例外を投げる
AssertThrows(std::out_of_range, empty.at(1));
// 最後に例外で投げられたstd::out_of_range型オブジェクトはwhat()メンバ関数で以下の述語を満たす
AssertThat(LastException<std::out_of_range>().what(), Is().EqualTo("invalid vector<T> subscript")); // VS2015の場合のメッセージ
  • カスタムMatcher

snowhouseはカスタムMatcherをサポートしています。例として偶数か判定するIsEvenを作ってみたいと思います。

struct IsEven
{
    // 戻り値型がboolで引数を1つ取るMatchesというconstメンバ関数を定義する
    template <typename Integral>
    inline bool Matches(Integral const& value) const
    {
        return value % 2 == 0;
    }

    // 出力ストリーム演算子に対応する
    friend std::ostream& operator<<(std::ostream& os, IsEven const&)
    {
        // ここにExpected(期待する状態)は何か記述する。エラー時に出力される
        os << "An Even Inegral";
        return os;
    }
};

go_bandit([] {
    describe("テスト", [&] {
        it("成功するべき", [&] {
            // 2は偶数である
            AssertThat(2, Fulfills(IsEven{}));
            AssertThat(2, Is().Fulfilling(IsEven{}));
        });
    });
});

カスタムMatcherは検証を行うMatchesメンバ関数と、出力ストリームに対応する必要があります。

ここで全てのMatcherを紹介したわけではありません。紹介していないMatcherはありますし、もしかしたらアンドキュメントのMatcherがあるかもしれません。 流石にJavaのHamcrestと比べるとMatcherが物足りなく感じますが、必要最低限は揃っていると思います。足りなければカスタムMatcherで補いましょう。

最後に

後半banditというよりはsnowhouseの紹介になってしまいました。C++11前提でデザインされ、RSpecライクな構造でJUnit4以降ライクなアサーションを記述できるのはなかなか快適なのではないかと思いました。xunit形式のxml出力に対応しているのも嬉しいですね。まだ実用したことがないので今度個人開発で使ってみようと思います。その時また感想を報告できたらと思います。*2

*1:operator==の日本語訳は等価演算子が多いので等価にしました。等値とどっちにするか迷いました

*2:本当はstd::vectorのテストのサンプルを書いて閉めようと思ったのですがこの記事書き始めてから4時間以上経って疲れたのでここまでにします……