-
-
Save dario-l/2d5b015fdebc1bde8de11535590e37bf to your computer and use it in GitHub Desktop.
Async Aggregate Source Command Handler Testing ... in a gist
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public delegate IHandleCommand<object> HandlerFactory(UnitOfWork unitOfWork, IMemoryEventStore eventStore, object message); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface IMemoryEventStore | |
{ | |
Fact[] All { get; } | |
object[] Read(string identifier); | |
void Write(string identifier, object[] events); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface ITestableMemoryEventStore | |
{ | |
void Initialize(Fact[] facts); | |
Fact[] GetChanges(); | |
void ClearChanges(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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