自律テストのトピックは、骨に分解された、長年にわたる立派なものです。 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スタイルで実装されます。
- テストされたオブジェクト(および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);
- テストケース実行
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; }); }
- クレームの検証:
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); }
まとめ
古い学校と比較した新しいアプローチのパン:
- コメントではなくコード
- コンパイラが理解して制御するものは、人が理解して制御するものに近いものです。
- より良い簡潔さ、表現力、読みやすさ。
- 繰り返しアクションは、別々の方法で簡単かつ快適に際立っています。
- 通常のテストと同じスタイルでスローされた例外にモックとアサーションを使用することがサポートされています
高レベルのBDDフレームワークと比較したバンズ:
- 儀式と冗長性が何倍も少なくなります。
- テストを容易にする他のライブラリに関する直交性。
- テスト言語は通常のC#であり、スタジオのすべての力と多くの開発者によってサポートされています。
補足と批判は伝統的に歓迎されています。