The topic of offline testing is a long-standing, venerable, disassembled to the bone. It seems that after the excellent book of Roy Osherow there is nothing special to say. But in my opinion there is some imbalance in the available tools. On the one hand, monsters like SpecFlow , with a huge overhead for the ability to write test-specifications in quasi-natural language, on the other - the Chelyabinsk severity of old-school frameworks like NUnit . What is missing? A tool for a concise, expressive, easy-to-read test record, for convenience and orthogonality similar to libraries for creating fakes, such as FakeItEasy , or for checking statements like FluentAssertion .
At the moment I am trying to create such a tool.
Here is a typical test using my microbibliography:
[Test] public void GivenSelfUsableWhenDisposeThenValueShouldBeDisposed() { Given(A.Fake<IDisposable>().ToUsable()). When(_ => _.Dispose()). Then(_ => _.Value.ShouldBeDisposed()); }
The FakeItEasy and FluentAssertions libraries are also involved, not as dependencies, but each for solving their own problems (fake and claims validation).
The equivalent code in the old school style is:
[Test] public void GivenSelfUsableWhenDisposeThenValueShouldBeDisposed() { // Arrange var usable = A.Fake<IDisposable>().ToUsable(); // Act usable.Dispose(); // Assert usable.Value.ShouldBeDisposed(); }
But that is not all. Suppose we have a mock - a fake, for which, after executing the test script, we make verification of statements. According to OsherĂłw there should be no more than one per test.
Code in new style:
[Test] public void GivenNeutralUsableWhenDisposeThenValueShouldBeNotDisposed() { Given(A.Fake<IDisposable>()). And(mock => mock.ToNeutralUsable()). When(_ => _.Dispose()). ThenMock(_ => _.ShouldBeNotDisposed()); }
Using the And method, the result of the previous Given is fixed as mock, and the result of the delegate’s work becomes the test object. This is logical, since the mock is used in the test object and it is natural to create it earlier.
Often statements include both mock and test object. This option is also supported:
[Test] public void GivenObjectWhenToUsableThenValueShouldBeSameAsObject() { Given(A.Fake<object>()). And(mock => mock.ToUsable(A.Dummy<IDisposable>())). When(_ => _). Then((_, mock) => _.Value.Should().Be.SameAs(mock)); }
Very often, a test in which a checked statement includes throwing an exception looks very cumbersome and unreadable against the background of "clean" options. The new approach allows checking exceptions both succinctly and stylistically consistently with “smooth” tests.
[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>()); }
In addition, this code is visible ...
Using the And extension method, you can add additional actions and assertion checks (performed in the order of recording method calls). This allows you to conveniently structure the test code.
An ax in the microbial library is such a class:
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; } }
Separate stages of testing correspond to his heirs.
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; } }
Implementation inheritance is designed in accordance with the recommendations from my previous article .
All visible magic is implemented in LINQ-style using generalized extension methods.
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); }); }
In the code examples, a class from my Disposable article without limits and several extension methods were tested for durability. At the moment, the class has been renamed from Disposable to Usable to avoid name collisions with a commonly used pattern.
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); }
Buns new approach compared with the old school:
Buns compared to high-level BDD frameworks:
Additions and criticism are traditionally welcome.
Source: https://habr.com/ru/post/310942/
All Articles