📜 ⬆️ ⬇️

Ultralight BDD: Little Mechanization of Stand-Alone Tests

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.


Bdd ax


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(); } 

Support mocks


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)); } 

Exception support


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 ...


Support for additional actions and approvals.


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.


Secret ingredient


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 .


Seasonings


All visible magic is implemented in LINQ-style using generalized extension methods.


  1. Creating a test object (and mock)
     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. Test case run
     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. Claims Verification:
     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); }); } 

Guinea pig


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); } 

Results


Buns new approach compared with the old school:


  1. Code instead of comments
  2. What the compiler understands and controls is close to what the person understands and controls.
  3. Better and conciseness, and expressiveness, and readability.
  4. Repetitive actions stand out in separate methods easily and pleasantly.
  5. In the same style as usual tests, the use of mocks and statements for thrown exceptions is supported.

Buns compared to high-level BDD frameworks:


  1. At times less ceremonies and verbiage.
  2. Orthogonality with respect to other libraries that facilitate testing.
  3. The test language is plain C #, supported by all the power of the studio and an army of developers.

Additions and criticism are traditionally welcome.


')

Source: https://habr.com/ru/post/310942/


All Articles