2つを組み合わせて使ったら便利だったのでメモ。
ちなみに2つとも日本語記事は全然見かけなかったので、なんで日本では流行らないのかなーとちょっと悲しかった(´・ω・`)
流行ることを願ってタグ振っておきます。
※わかる方とか想像つく方いましたらコメントください
経緯
- もともとNUnit使っていたところに興味からXUnitを使ってみた
- アサーションエラーのメッセージは
Assert.True()
とか一部のメソッドにしか実装されてない - NUnitではコメント書く派だったので何かやり方ないかなと『xunit message』あたりで検索してみる
- 某英語のQAサイト で紹介あったものを使ってみたら便利だった
問題点
経緯にも記載してるけど、アサーションエラーのメッセージがほとんど書けない 実装になっているらしい。
[Fact]
public void T01Equalでアサーション() {
var expValue = 2;
var actual1 = Add1(1); // 1を足すメソッドを想定
// Assert.Equal(expValue, actual1, "actual1 がおかしい"); // <= これが記述できない
Assert.Equal(expValue, actual1);
var actual2 = Add1(2);
Assert.Equal(expValue, actual2); // ここで失敗する
}
X XUnitSample.SampleTests.T01Equalでアサーション [6ms]
エラー メッセージ:
Assert.Equal() Failure
Expected: 2
Actual: 3
Assert.True()
や Assert.False()
はメッセージが書けるけど、エラー時の具体的な値が見えなくなってしまう。
[Fact]
public void T02Trueでアサーション() {
var expValue = 2;
var actual1 = Add1(1);
Assert.True(expValue == actual1, "actual1がおかしい");
var actual2 = Add1(2);
Assert.True(expValue == actual2, "actual2がおかしい"); // ここで失敗する
}
X XUnitSample.SampleTests.T02Trueでアサーション [5ms]
エラー メッセージ:
actual2がおかしい
Expected: True
Actual: False
ライブラリ
ライブラリ全体の機能は多いので、実際に試した範囲のみを取り上げます。
(要約:ちゃんと使いこなしてはいません)
xBehave.net
ライセンス:MIT License
自然言語でシナリオの説明を記載することができるライブラリ。
Use natural language to describe your scenarios.
具体的には string に拡張メソッド生やして、元の string にエラーメッセージ(というかケース)を記載していくイメージ。
string.x(Action)
内のアクションがそれぞれの説明内容のステップとして実行される。
ケースをまたがる変数についてはローカル変数またはシナリオメソッドの引数として宣言する。
※ TheoryData
の代替となる Example
を使いたい場合は引数で宣言すること。
[Scenario] // <= Factの代わりにxBehaveの属性を使用
public void T03Scenarioでアサーション() {
int expValue = 0;
int actual;
"値初期化"
.x(() => { expValue = 2; });
"引数1の結果を確認"
.x(() => { actual = Add1(1); Assert.Equal(expValue, actual); });
"引数2の結果を確認"
.x(() => { actual = Add1(2); Assert.Equal(expValue, actual); }); // ここで失敗する
"引数3の結果を確認"
.x(() => { actual = Add1(3); Assert.Equal(expValue, actual); });
}
// public void T03~~(int expValue, int actual) としてローカル変数なしでもよい
テストがステップ毎に検証され、エラーが起きると付加情報としてステップ名が一緒に表示される。
X XUnitSample.SampleTests.T03Scenarioでアサーション() [03] 引数2の結果を確認 [1ms]
エラー メッセージ:
Assert.Equal() Failure
Expected: 2
Actual: 3
! XUnitSample.SampleTests.T03Scenarioでアサーション() [04] 引数3の結果を確認 [1ms]
ちなみに Visual Studio のテストエクスプローラーからテストを実行すると、
- どこまでが成功してどこで失敗したか、どのステップが処理されてないか
- 各ステップ内での処理時間
についてテスト毎に メソッド内部のステップ単位で表示される のでさらに便利。
注意事項
Documentation > Writing scenarios のNoteに記載されているが、テスト処理は値の代入だろうがAssertだろうが
すべて string.x()
の内部で記述する必要がある。
Note: You should not be writing any code inside a scenario method which is outside one of the step definitions.
It doesn't make sense to do so, since a scenario method only exists in order to define the steps, and it is executed
in a context which makes that assumption.
[Scenario]
public void T04Scenarioの誤った書き方() {
int expValue = 2;
int actual = Add1(1); // ここでは2になる
"引数1の結果を確認"
.x(() => { Assert.Equal(expValue, actual); });
actual = Add1(2); // ここでは3になる
"引数2の結果を確認"
.x(() => { Assert.Equal(expValue, actual); }); // ここで失敗するはず
}
テストケースのメソッドが全て処理されてから各ステップが処理されるらしい。
X XUnitSample.SampleTests.T04Scenarioの誤った書き方() [01] 引数1の結果を確認 [2ms]
エラー メッセージ:
Assert.Equal() Failure
Expected: 2
Actual: 3
! XUnitSample.SampleTests.T04Scenarioの誤った書き方() [02] 引数2の結果を確認 [1ms]
ダイナミックなテストケース(非公式)
a scenario method only exists in order to define the steps
を逆手に取った使い方。
公式の Documentation にはそれっぽいことが記載されていなかったけどやってみたらできたのでメモ。
[Scenario]
public void T21ダイナミックなテストステップ() {
Enumerable.Range(0, 10)
.ToList()
.ForEach(i => {
$"{i} が5以下であるか確認"
.x(() => i.Should().BeLessOrEqualTo(5));
});
}
ステップが動的に作成されてそれぞれテストされている。
X XUnitSample.SampleTests.T21ダイナミックなテストステップ() [07] 6 が5以下であるか確認 [1ms]
エラー メッセージ:
Expected i to be less or equal to 5, but found 6.
! XUnitSample.SampleTests.T21ダイナミックなテストステップ() [08] 7 が5以下であるか確認 [1ms]
! XUnitSample.SampleTests.T21ダイナミックなテストステップ() [09] 8 が5以下であるか確認 [1ms]
! XUnitSample.SampleTests.T21ダイナミックなテストステップ() [10] 9 が5以下であるか確認 [1ms]
具体的な使い方としては、以下のようなテスト処理を用意しておくとインファイルとアウトファイル用意するだけでいろんなパターンの検証追加したりできそう。(未検証)
var inDir = new DirectoryInfo("input");
var outDir = "output";
input.EnumerateFiles("*.txt").ToList().ForEach(file => {
var outFile = Path.Combine(outDir, file.Name);
// TargetFuncがテスト対象の処理
$"テストデータ {file.Name} の確認"
.x(()=> TartetFunc(File.ReadAllText(file.FullName))
.Should().Be(File.ReadAllText(outFile)));
});
Fluent Assersions
ライセンス:Apache License 2.0
テストの期待値を自然な形で指定できるようにするメソッド拡張のライブラリ。
A very extensive set of extension methods that allow you to more naturally specify the expected outcome of a TDD or BDD-style unit tests.
具体的には各データ型に Should()
拡張メソッドを生やしてアサーションラッパーを生成、ラッパーに対して各アサーションメソッドを呼び出すことで検証していく。
[Fact]
public void T05Fluentアサーション() {
var expValue = 2;
var actual1 = Add1(1);
actual1.Should().Be(expValue, "期待値と違う");
var actual2 = Add1(2);
actual2.Should().Be(expValue, "期待値と違う"); // ここで失敗する
}
エラーメッセージも独自に整形されるのだが、嬉しいのが Should()
の呼び出し元 がメッセージに表示されること。
どうやってるんだろうこれ(*´ω`)
X XUnitSample.SampleTests.T05Fluentアサーション [95ms]
エラー メッセージ:
Expected actual2 to be 2 because 期待値と違う, but found 3.
なお、変数でなく数式やメソッドチェーンでも行ける模様。
(actual2 + 3).Should().Be(expValue + 3, "期待値と違う");
// => Expected (actual2 + 3) to be 5 because 期待値と違う, but found 6.
一つのアサーションラッパーに対して複数の検証を行うことも可能。
[Fact]
public void T06Fluentのチェーン() {
var actual = new[] { 1, 3, 5, 7, 9 };
actual.Should().NotBeEmpty()
.And.HaveCount(5)
.And.ContainInOrder(1, 7, 5);
}
X XUnitSample.SampleTests.T06Fluentのチェーン [115ms]
エラー メッセージ:
Expected actual {1, 3, 5, 7, 9} to contain items {1, 7, 5} in order, but 5 (index 2) did not appear (in the right order).
また Should()
は元の型に対して異なるラッパーを返すので、型に応じてある程度自然に記述できるようになっている。
ただしこの辺は特性慣れないとハマることもあるかもしれないので注意。
[Fact]
public void T07型毎のShouldの違い() {
var actual1 = new[] { 1, 3, 5, 7, 9 };
actual1.Should().Equal(1, 3, 5, 7, 9); // コレクションに対しては要素それぞれの一致を検証
var actual2 = Add1(2);
actual2.Should().Equals(3); // 数値に対しては単体の等価を検証
actual2.Should().BeGreaterThan(2); // 数値のラッパーで利用可能なメソッド
// actual1.Should().BeGreaterThan(2); // <= コレクションに対しては実装されていない
}
組み合わせて実装
[Scenario]
public void T11xBehaveとFluent組み合わせ() {
int expValue = 0;
"値初期化"
.x(() => { expValue = 2; });
"引数の結果を確認"
.x(() => {
Add1(1).Should().Be(expValue);
Add1(2).Should().Be(expValue); // ここで失敗する。
Add1(3).Should().Be(expValue);
});
"ここは処理されない"
.x(() => { });
}
どのステップで失敗して具体的にどの検証に対してNGなのかがわかりやすくなって嬉しい(๑•̀ㅂ•́)و✧
X XUnitSample.SampleTests.T11xBehaveとFluent組み合わせ() [02] 引数の結果を確認 [2ms]
エラー メッセージ:
Expected Add1(2) to be 2, but found 3.
! XUnitSample.SampleTests.T11xBehaveとFluent組み合わせ() [03] ここは処理されない [1ms]
参考まとめ
- xBehave.net
- Fluent Assersions
-
stack overflow - How to implement XUnit descriptive Assert message?
- 同じこと考えた人がいたらしく、いろいろ
コレジャナイ案とかもあって回答やアプローチがあって参考になりました
- 同じこと考えた人がいたらしく、いろいろ