概要
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はrubyのRSpecやjavascriptの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