8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

xBehave.net + Fluent Assertionsを使ったら思いの外便利だった

Posted at

2つを組み合わせて使ったら便利だったのでメモ。
ちなみに2つとも日本語記事は全然見かけなかったので、なんで日本では流行らないのかなーとちょっと悲しかった(´・ω・`)
流行ることを願ってタグ振っておきます。
※わかる方とか想像つく方いましたらコメントください

経緯

  • もともとNUnit使っていたところに興味からXUnitを使ってみた
  • アサーションエラーのメッセージは Assert.True() とか一部のメソッドにしか実装されてない
  • NUnitではコメント書く派だったので何かやり方ないかなと『xunit message』あたりで検索してみる
  • 某英語のQAサイト で紹介あったものを使ってみたら便利だった

問題点

経緯にも記載してるけど、アサーションエラーのメッセージがほとんど書けない 実装になっているらしい。

Equalメソッドはメッセージが書けない
[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); // ここで失敗する
}
テスト結果1、ただ失敗したとしか出ない
  X  XUnitSample.SampleTests.T01Equalでアサーション [6ms]
  エラー メッセージ:
   Assert.Equal() Failure
Expected: 2
Actual:   3

Assert.True()Assert.False() はメッセージが書けるけど、エラー時の具体的な値が見えなくなってしまう。

テスト実装2、Trueはメッセージが書けるけど失敗した値がわからない
[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がおかしい"); // ここで失敗する
}
テスト結果2、どんな値の整合で間違ったのか見えない
  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 を使いたい場合は引数で宣言すること。

テスト実装3、xBehaveのScenarioでの実装
[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) としてローカル変数なしでもよい

テストがステップ毎に検証され、エラーが起きると付加情報としてステップ名が一緒に表示される。

テスト結果3
  X XUnitSample.SampleTests.T03Scenarioでアサーション() [03] 引数2の結果を確認 [1ms]
  エラー メッセージ:
   Assert.Equal() Failure
Expected: 2
Actual:   3
  ! XUnitSample.SampleTests.T03Scenarioでアサーション() [04] 引数3の結果を確認 [1ms]

ちなみに Visual Studio のテストエクスプローラーからテストを実行すると、

  • どこまでが成功してどこで失敗したか、どのステップが処理されてないか
  • 各ステップ内での処理時間

についてテスト毎に メソッド内部のステップ単位で表示される のでさらに便利。

テストエクスプローラーでScenarioメソッドの結果を表示

注意事項

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.

テスト実装4、意図した処理にならない例
[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); }); // ここで失敗するはず
}

テストケースのメソッドが全て処理されてから各ステップが処理されるらしい。

テスト結果4、『引数1の結果を確認』の段階でactualが3になっている
  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() 拡張メソッドを生やしてアサーションラッパーを生成、ラッパーに対して各アサーションメソッドを呼び出すことで検証していく。

テスト実装5、基本的な使い方
[Fact]
public void T05Fluentアサーション() {
    var expValue = 2;
    var actual1 = Add1(1);
    actual1.Should().Be(expValue, "期待値と違う");
    var actual2 = Add1(2);
    actual2.Should().Be(expValue, "期待値と違う"); // ここで失敗する
}

エラーメッセージも独自に整形されるのだが、嬉しいのが Should() の呼び出し元 がメッセージに表示されること。
どうやってるんだろうこれ(*´ω`)

テスト結果5、呼び出し元の名称を含めてエラーメッセージが生成される
  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.

一つのアサーションラッパーに対して複数の検証を行うことも可能。

テスト実装6、
[Fact]
public void T06Fluentのチェーン() {
    var actual = new[] { 1, 3, 5, 7, 9 };
    actual.Should().NotBeEmpty()
        .And.HaveCount(5)
        .And.ContainInOrder(1, 7, 5);
}
テスト結果6
  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() は元の型に対して異なるラッパーを返すので、型に応じてある程度自然に記述できるようになっている。
ただしこの辺は特性慣れないとハマることもあるかもしれないので注意。

テスト実装7、型毎の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); // <= コレクションに対しては実装されていない
}

組み合わせて実装

テスト実装8、組み合わせ
[Scenario]
public void T11xBehaveFluent組み合わせ() {
    int expValue = 0;
    "値初期化"
        .x(() => { expValue = 2; });
    "引数の結果を確認"
        .x(() => {
            Add1(1).Should().Be(expValue);
            Add1(2).Should().Be(expValue); // ここで失敗する。
            Add1(3).Should().Be(expValue);
        });
    "ここは処理されない"
        .x(() => { });
}

どのステップで失敗して具体的にどの検証に対してNGなのかがわかりやすくなって嬉しい(๑•̀ㅂ•́)و✧

テスト結果8
  X XUnitSample.SampleTests.T11xBehaveとFluent組み合わせ() [02] 引数の結果を確認 [2ms]
  エラー メッセージ:
   Expected Add1(2) to be 2, but found 3.
  ! XUnitSample.SampleTests.T11xBehaveとFluent組み合わせ() [03] ここは処理されない [1ms]

参考まとめ

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?