Skip to content

Instantly share code, notes, and snippets.

@dario-l
Forked from yreynhout/Catch.cs
Created May 9, 2017 20:45
Show Gist options
  • Save dario-l/2d5b015fdebc1bde8de11535590e37bf to your computer and use it in GitHub Desktop.
Save dario-l/2d5b015fdebc1bde8de11535590e37bf to your computer and use it in GitHub Desktop.
Async Aggregate Source Command Handler Testing ... in a gist
using System;
using System.Threading.Tasks;
public static class Catch
{
public static async Task<Optional<Exception>> ExceptionAsync(Func<Task> action)
{
var result = Optional<Exception>.Empty;
try
{
await action();
}
catch(Exception exception)
{
result = new Optional<Exception>(exception);
}
return result;
}
}
public delegate IHandleCommand<object> HandlerFactory(UnitOfWork unitOfWork, IMemoryEventStore eventStore, object message);
using System;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Represents an event centric test specification runner.
/// </summary>
public interface IAsyncEventCentricTestSpecificationRunner
{
/// <summary>
/// Runs the specified test specification.
/// </summary>
/// <param name="specification">The test specification to run.</param>
/// <param name="cancellationToken">The task cancellation token.</param>
/// <returns>The result of running the test specification.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="specification"/> is <c>null</c>.</exception>
Task<EventCentricTestResult> RunAsync(EventCentricTestSpecification specification, CancellationToken cancellationToken);
}
using System;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Represents an exception centric test specification runner.
/// </summary>
public interface IAsyncExceptionCentricTestSpecificationRunner
{
/// <summary>
/// Runs the specified test specification asynchronously.
/// </summary>
/// <param name="specification">The test specification to run.</param>
/// <param name="cancellationToken">The task cancellation token.</param>
/// <returns>The result of running the test specification.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="specification"/> is <c>null</c>.</exception>
Task<ExceptionCentricTestResult> RunAsync(ExceptionCentricTestSpecification specification, CancellationToken cancellationToken);
}
public interface IMemoryEventStore
{
Fact[] All { get; }
object[] Read(string identifier);
void Write(string identifier, object[] events);
}
public interface ITestableMemoryEventStore
{
void Initialize(Fact[] facts);
Fact[] GetChanges();
void ClearChanges();
}
public class MemoryEventStore : ITestableMemoryEventStore, IMemoryEventStore
{
private readonly List<Fact> _givens;
private readonly List<Fact> _thens;
public Fact[] All
{
get { return _givens.Concat(_thens).ToArray(); }
}
public MemoryEventStore()
{
_givens = new List<Fact>();
_thens = new List<Fact>();
}
public void Initialize(Fact[] facts)
{
if(facts == null)
{
throw new ArgumentNullException("facts");
}
_givens.AddRange(facts);
}
public Fact[] GetChanges()
{
return _thens.ToArray();
}
public void ClearChanges()
{
_thens.Clear();
}
public object[] Read(string identifier)
{
if(identifier == null)
{
throw new ArgumentNullException("identifier");
}
return _givens.Concat(_thens).
Where(_ => _.Identifier == identifier).
Select(_ => _.Event).
ToArray();
}
public void Write(string identifier, object[] events)
{
if(identifier == null)
{
throw new ArgumentNullException("identifier");
}
if(events == null)
{
throw new ArgumentNullException("events");
}
_thens.AddRange(events.Select(@event => new Fact(identifier, @event)));
}
}
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Sdk;
public static class ScenarioExtensions
{
public static Task AssertUsing(this IExceptionCentricTestSpecificationBuilder builder, IAsyncExceptionCentricTestSpecificationRunner runner,
[CallerMemberName] string scenario = "")
{
return builder.AssertUsing(runner, CancellationToken.None, scenario);
}
public static async Task AssertUsing(this IExceptionCentricTestSpecificationBuilder builder, IAsyncExceptionCentricTestSpecificationRunner runner,
CancellationToken cancellationToken, [CallerMemberName] string scenario = "")
{
var specification = builder.Build();
var result = await runner.RunAsync(specification, cancellationToken);
if(result.Failed)
{
var messageBuilder = new StringBuilder();
if(result.ButException.HasValue)
{
messageBuilder.AppendLine();
messageBuilder.AppendFormat("Expected: {0}", specification.Throws);
messageBuilder.AppendLine();
messageBuilder.AppendFormat("But was : {0}", result.ButException.Value);
messageBuilder.AppendLine();
messageBuilder.AppendFormat("For scenario: {0}", scenario);
messageBuilder.AppendLine();
new TestSpecificationWriter(messageBuilder).Write(specification);
throw new XunitException(messageBuilder.ToString());
}
if(result.ButEvents.HasValue)
{
messageBuilder.AppendLine();
messageBuilder.AppendFormat("Expected: {0}", specification.Throws);
messageBuilder.AppendLine();
messageBuilder.AppendFormat("But were: {0} events ({1})",
result.ButEvents.Value.Length,
string.Join(",", result.ButEvents.Value.Select(_ => _.Event.GetType().Name)));
messageBuilder.AppendLine();
messageBuilder.AppendFormat("For scenario: {0}", scenario);
messageBuilder.AppendLine();
new TestSpecificationWriter(messageBuilder).Write(specification);
throw new XunitException(messageBuilder.ToString());
}
messageBuilder.AppendLine();
messageBuilder.AppendFormat("Expected: {0}", specification.Throws);
messageBuilder.AppendLine();
messageBuilder.Append("But no exception was thrown.");
messageBuilder.AppendLine();
messageBuilder.AppendFormat("For scenario: {0}", scenario);
messageBuilder.AppendLine();
new TestSpecificationWriter(messageBuilder).Write(specification);
throw new XunitException(messageBuilder.ToString());
}
}
public static Task AssertUsing(this IEventCentricTestSpecificationBuilder builder, IAsyncEventCentricTestSpecificationRunner runner,
[CallerMemberName] string scenario = "")
{
return builder.AssertUsing(runner, CancellationToken.None, scenario);
}
public static async Task AssertUsing(this IEventCentricTestSpecificationBuilder builder, IAsyncEventCentricTestSpecificationRunner runner,
CancellationToken cancellationToken, [CallerMemberName] string scenario = "")
{
var specification = builder.Build();
var result = await runner.RunAsync(specification, cancellationToken);
if(result.Failed)
{
var messageBuilder = new StringBuilder();
if(result.ButException.HasValue)
{
messageBuilder.AppendLine();
messageBuilder.AppendFormat("Expected: {0} events ({1})",
specification.Thens.Length,
string.Join(",", specification.Thens.Select(_ => _.Event.GetType().Name)));
messageBuilder.AppendLine();
messageBuilder.AppendFormat("But was : {0}", result.ButException.Value);
messageBuilder.AppendLine();
messageBuilder.AppendFormat("For scenario: {0}", scenario);
messageBuilder.AppendLine();
new TestSpecificationWriter(messageBuilder).Write(specification);
throw new XunitException(messageBuilder.ToString());
}
if(result.ButEvents.HasValue)
{
if(result.ButEvents.Value.Length != specification.Thens.Length)
{
messageBuilder.AppendLine();
messageBuilder.AppendFormat("Expected: {0} events ({1})",
specification.Thens.Length,
string.Join(",", specification.Thens.Select(_ => _.Event.GetType().Name)));
messageBuilder.AppendLine();
messageBuilder.AppendFormat("But were: {0} events ({1})",
result.ButEvents.Value.Length,
string.Join(",", result.ButEvents.Value.Select(_ => _.Event.GetType().Name)));
messageBuilder.AppendLine();
messageBuilder.AppendFormat("For scenario: {0}", scenario);
messageBuilder.AppendLine();
new TestSpecificationWriter(messageBuilder).Write(specification);
throw new XunitException(messageBuilder.ToString());
}
messageBuilder.AppendLine();
messageBuilder.AppendLine("Expected:");
var index = 0;
foreach(var fact in specification.Thens)
{
if(index > 0)
{
messageBuilder.AppendLine();
}
messageBuilder.AppendFormat(" [{0}]:{1}", index, fact.Event);
index++;
}
messageBuilder.AppendLine();
messageBuilder.AppendLine("But were:");
index = 0;
foreach(var fact in result.ButEvents.Value)
{
if(index > 0)
{
messageBuilder.AppendLine();
}
messageBuilder.AppendFormat(" [{0}]:{1}", index, fact.Event);
index++;
}
messageBuilder.AppendLine();
messageBuilder.AppendFormat("For scenario: {0}", scenario);
messageBuilder.AppendLine();
new TestSpecificationWriter(messageBuilder).Write(specification);
throw new XunitException(messageBuilder.ToString());
}
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
public class TestSpecificationRunner : IAsyncExceptionCentricTestSpecificationRunner, IAsyncEventCentricTestSpecificationRunner
{
private readonly HandlerFactory _factory;
private readonly ITestableMemoryEventStore _eventStore;
private readonly IEqualityComparer<Exception> _exceptionComparer;
private readonly IEqualityComparer<Fact> _factComparer;
public TestSpecificationRunner(HandlerFactory factory, ITestableMemoryEventStore eventStore, IEqualityComparer<Exception> exceptionComparer,
IEqualityComparer<Fact> factComparer)
{
if(factory == null)
{
throw new ArgumentNullException("factory");
}
if(eventStore == null)
{
throw new ArgumentNullException("eventStore");
}
if(exceptionComparer == null)
{
throw new ArgumentNullException("exceptionComparer");
}
if(factComparer == null)
{
throw new ArgumentNullException("factComparer");
}
_factory = factory;
_eventStore = eventStore;
_exceptionComparer = exceptionComparer;
_factComparer = factComparer;
}
public async Task<ExceptionCentricTestResult> RunAsync(ExceptionCentricTestSpecification specification, CancellationToken cancellationToken)
{
if(specification == null)
{
throw new ArgumentNullException("specification");
}
_eventStore.Initialize(specification.Givens);
var unitOfWork = new UnitOfWork();
var handler = _factory(unitOfWork, _eventStore, specification.When);
var exception = await Catch.ExceptionAsync(() => handler(specification.When, cancellationToken));
var changes = unitOfWork.
GetChanges().
SelectMany(aggregate =>
aggregate.Root.
GetChanges().
Select(@event => new Fact(aggregate.Identifier, @event))).
ToArray();
if(exception.HasValue)
{
if(!_exceptionComparer.Equals(exception.Value, specification.Throws))
{
return specification.Fail(exception.Value);
}
return specification.Pass();
}
return changes.Length != 0 ? specification.Fail(changes) : specification.Fail();
}
public async Task<EventCentricTestResult> RunAsync(EventCentricTestSpecification specification, CancellationToken cancellationToken)
{
if(specification == null)
{
throw new ArgumentNullException("specification");
}
_eventStore.Initialize(specification.Givens);
var unitOfWork = new UnitOfWork();
var handler = _factory(unitOfWork, _eventStore, specification.When);
var exception = await Catch.ExceptionAsync(() => handler(specification.When, cancellationToken));
var changes = unitOfWork.
GetChanges().
SelectMany(aggregate =>
aggregate.Root.
GetChanges().
Select(@event => new Fact(aggregate.Identifier, @event))).
ToArray();
if(exception.HasValue)
{
return specification.Fail(exception.Value);
}
return !changes.SequenceEqual(specification.Thens, _factComparer) ? specification.Fail(changes) : specification.Pass();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment