超軽量BDD:スタンドアロンテストの小さな機械化

自律テストのトピックは、骨に分解された、長年にわたる立派なものです。 Roy Oscherowvのすばらしいの後、言うべきことはほとんどないようです。 しかし、私の意見では、利用可能なツールにいくつかの不均衡があります。 一方で、 SpecFlowのようなモンスターは、準自然言語でテスト仕様を記述できるために大きなオーバーヘッドを持ち、他方では、 NUnitのような古い学校のフレームワークのチェリャビンスクの厳しさです。 何が欠けていますか? FakeItEasyなどの偽物を作成したり、 FluentAssertionなどのクレームをチェックしたりするライブラリに似た、簡潔で表現力に富んだ読みやすいテスト記録ツール









現在、このようなツールを作成しようとしています。







xからのBDD



マイクロライブラリを使用した典型的なテストを次に示します。







[Test] public void GivenSelfUsableWhenDisposeThenValueShouldBeDisposed() { Given(A.Fake<IDisposable>().ToUsable()). When(_ => _.Dispose()). Then(_ => _.Value.ShouldBeDisposed()); }
      
      





ライブラリFakeItEasyおよびFluentAssertionsも含まれますが、依存関係としてではなく、それぞれ独自の問題(偽物とクレームの検証)を解決します。

同等の旧式のスタイルコード:







 [Test] public void GivenSelfUsableWhenDisposeThenValueShouldBeDisposed() { // Arrange var usable = A.Fake<IDisposable>().ToUsable(); // Act usable.Dispose(); // Assert usable.Value.ShouldBeDisposed(); }
      
      





模擬サポート



しかし、それだけではありません。 モック(偽物)があり、テストシナリオを実行した後、ステートメントの検証を行うとします。 Osherowによると、テストごとに1つしか存在しないはずです。







新しいスタイルのコード:







 [Test] public void GivenNeutralUsableWhenDisposeThenValueShouldBeNotDisposed() { Given(A.Fake<IDisposable>()). And(mock => mock.ToNeutralUsable()). When(_ => _.Dispose()). ThenMock(_ => _.ShouldBeNotDisposed()); }
      
      





Andメソッドを使用すると、前のGivenの結果はモックとして固定され、デリゲートの結果はテストオブジェクトになります。 モックはテスト対象のオブジェクトで使用されており、それを以前に作成するのが自然なので、これは論理的です。







多くの場合、クレームにはmokとテストオブジェクトの両方が含まれます。 このオプションもサポートされています:







 [Test] public void GivenObjectWhenToUsableThenValueShouldBeSameAsObject() { Given(A.Fake<object>()). And(mock => mock.ToUsable(A.Dummy<IDisposable>())). When(_ => _). Then((_, mock) => _.Value.Should().Be.SameAs(mock)); }
      
      





例外サポート



非常に多くの場合、検証可能なステートメントに例外のスローが含まれるテストは、「クリーン」オプションの背景に対して非常に面倒で読めないように見えます。 この新しいアプローチにより、「スムーズ」テストを使用して、例外を簡潔かつスタイル的に均一にチェックできます。







 [Test] public void GivenUsableWhenDisposeTwiceThenShouldBeException() { Given(A.Fake<IDisposable>()). And(mock => A.Dummy<object>().ToUsable(mock)). When(_ => _.Dispose()). And(_ => _.Dispose()). ThenCatch(e => e.Should().Be.OfType<ObjectDisposedException>()); }
      
      





また、このコードでは目に見える...







追加のアクションとステートメントのサポート。



And拡張メソッドを使用すると、追加のアクションとアサーションチェックを追加できます(メソッド呼び出しが記録される順序で実行されます)。 これにより、テストコードを簡単に構成できます。







秘密の成分



次のクラスは、マイクロライブラリ内のxで機能します。







 public abstract class GivenWhenThenBase<T, TMock> { internal GivenWhenThenBase(T result, TMock mock) { Result = result; Mock = mock; } internal T Result { get; set; } internal TMock Mock { get; } }
      
      





テストの個々の段階は、その相続人に対応しています







 public sealed class GivenResult<T, TMock> : GivenWhenThenBase<T, TMock> { internal GivenResult(T result, TMock mock) : base(result, mock) {} } public sealed class WhenResult<T, TMock> : GivenWhenThenBase<T, TMock> { internal WhenResult(T result, TMock mock, Exception e = null) : base(result, mock) { Exception = e; } internal Exception Exception { get; set; } } public sealed class ThenResult<T, TMock> : GivenWhenThenBase<T, TMock> { internal ThenResult(T result, TMock mock, Exception e = null) : base(result, mock) { Exception = e; } internal Exception Exception { get; set; } }
      
      





実装の継承は、 以前の記事の推奨事項に従って設計されています







調味料



すべての目に見える魔法は、一般的な拡張メソッドを使用してLINQスタイルで実装されます。







  1. テストされたオブジェクト(およびmoka)の作成

     public static GivenResult<T, object> Given<T>(T result) => new GivenResult<T, object>(result, null);
          
          





     public static GivenResult<T, TMock> And<T, TMock>(this GivenResult<TMock, object> givenResult, Func<TMock, T> and) => new GivenResult<T, TMock>(and(givenResult.Result), givenResult.Result);
          
          



  2. テストケース実行

     public static WhenResult<TResult, TMock> When<T, TMock, TResult>(this GivenResult<T, TMock> givenResult, Func<T, TResult> when) { try { return new WhenResult<TResult, TMock>(when(givenResult.Result), givenResult.Mock); } catch (Exception e) { return new WhenResult<TResult, TMock>(default(TResult), givenResult.Mock, e); } }
          
          





     public static WhenResult<TResult, TMock> And<T, TMock, TResult>(this WhenResult<T, TMock> whenResult, Func<T, TMock, TResult> and) { if (whenResult.Exception != null) return new WhenResult<TResult, TMock>(default(TResult), whenResult.Mock, whenResult.Exception); try { return new WhenResult<TResult, TMock>(and(whenResult.Result, whenResult.Mock), whenResult.Mock); } catch (Exception e) { return new WhenResult<TResult, TMock>(default(TResult), whenResult.Mock, e); } }
          
          





     public static WhenResult<T, TMock> When<T, TMock>(this GivenResult<T, TMock> givenResult, Action<T> when) { return givenResult.When(o => { when(o); return o; }); }
          
          





     public static WhenResult<T, TMock> And<T, TMock>(this WhenResult<T, TMock> whenResult, Action<T, TMock> and) { return whenResult.And((o, m) => { and(o, m); return o; }); }
          
          



  3. クレームの検証:

     public static ThenResult<T, TMock> Then<T, TMock>(this WhenResult<T, TMock> whenResult, Action<T, TMock, Exception> then) { then(whenResult.Result, whenResult.Mock, whenResult.Exception); return new ThenResult<T, TMock>(whenResult.Result, whenResult.Mock, whenResult.Exception); }
          
          





     public static ThenResult<T, TMock> Then<T, TMock>(this WhenResult<T, TMock> whenResult, Action<T, TMock> then) { return whenResult.Then((r, m, e) => { e.Should().Be.Null(); then(r, m); }); }
          
          





     public static ThenResult<T, TMock> ThenMock<T, TMock>(this WhenResult<T, TMock> whenResult, Action<TMock> then) { return whenResult.Then((r, m, e) => { e.Should().Be.Null(); then(m); }); }
          
          





     public static ThenResult<T, TMock> Then<T, TMock>(this WhenResult<T, TMock> whenResult, Action<T, TMock> then) { return whenResult.Then((r, m, e) => { e.Should().Be.Null(); then(r, m); }); }
          
          





     public static ThenResult<T, TMock> ThenCatch<T, TMock>(this WhenResult<T, TMock> whenResult, Action<Exception> then) { return whenResult.Then((r, m, e) => { e.Should().Not.Be.Null(); then(e); }); }
          
          





実験ウサギ



コード例では、 Disposable Borderless記事のクラスといくつかの拡張メソッドの強度をテストしました。 現時点では、クラスはDisposableからUsableに名前が変更され、一般的に使用されるパターンとの名前の衝突を回避しています。







 public sealed class Usable<T> : IDisposable { internal Usable(T resource, IDisposable usageTime) { _usageTime = usageTime; Value = resource; } public void Dispose() => _usageTime.Dispose(); public T Value { get; } private readonly IDisposable _usageTime; }
      
      





 public static class UsableExtensions { public static Usable<T> ToUsable<T>(this T resource, IDisposable usageTime) => new Usable<T>(resource, usageTime); public static Usable<T> ToUsable<T>(this T resource) where T : IDisposable => resource.ToUsable(resource); public static Usable<T> ToNeutralUsable<T>(this T resource) => resource.ToUsable(Disposable.Empty); }
      
      





まとめ



古い学校と比較した新しいアプローチのパン:







  1. コメントではなくコード
  2. コンパイラが理解して制御するものは、人が理解して制御するものに近いものです。
  3. より良い簡潔さ、表現力、読みやすさ。
  4. 繰り返しアクションは、別々の方法で簡単かつ快適に際立っています。
  5. 通常のテストと同じスタイルでスローされた例外にモックとアサーションを使用することがサポートされています


高レベルのBDDフレームワークと比較したバンズ:







  1. 儀式と冗長性が何倍も少なくなります。
  2. テストを容易にする他のライブラリに関する直交性。
  3. テスト言語は通常のC#であり、スタジオのすべての力と多くの開発者によってサポートされています。


補足と批判は伝統的に歓迎されています。








All Articles