EventOutcomes is a free, open-source .NET library created to make it easier to write unit tests for event sourced applications. It is based on the idea presented by Greg Young:
GIVEN events
WHEN command
THEN events
EventOutcomes is MIT licensed.
- GIVEN events, WHEN command, THEN events
- Additional exception assertions
- Additional arrangements and assertions on services, whether their real or fake implementations
- Support for dependency injection
- Independent of Event Sourcing and CQRS frameworks selection
Let's start by creating the simplest possible unit test for an event-sourced application. Every test we will write later will be based on the following structure.
[Fact]
public async Task given_bread_ingredients_mixed_when_BakeDough_for_25_minutes_then_bread_baked_and_fantastic_smell_produced()
{
var id = Guid.NewGuid();
var test = Test.For(id)
.Given(new FlourAdded(id, 500), new WaterAdded(id, 300), new YeastAdded(id, 7), new SaltAdded(id, 2), new IngredientsMixed(id))
.When(new BakeDough(id, 25))
.ThenInAnyOrder(new BreadBaked(id, 1), new SmellProduced(id, TypeOfSmell.Fantastic));
await Tester.TestAsync(test, new MyCustomAdapter());
}
As you can see above, the last line includes a class MyCustomAdapter
, which is a custom implementation of EventOutcomes IAdapeter
interface. To start using EventOutcomes to write unit tests for your application, all you need to do is create your own implementation in your project.
There are many ways to implement Event Sourcing and CQRS – you can use one of the existing frameworks or create your own implementation. EventOutcomes is designed to be framework-agnostic, meaning it can be used with any event sourcing and CQRS frameworks. However, there is one requirement before you can use EventOutcomes: you must implement the IAdapter
interface. This interface acts as a common denominator between EventOutcomes and your event sourcing and CQRS frameworks of choice. Below is an explanation of what needs to be implemented in the interface.
IServiceProvider ServiceProvider { get; }
– Service provider for all the services that need to be injected into your application code.Task BeforeTestAsync();
– A method that is called before the test is executed. If scoped services are required, this is the ideal place to create a scope and assign scoped service provider to theServiceProvider
property.Task AfterTestAsync();
– A method that is called after the test is completed. This is where any cleanup code should go.Task SetGivenEventsAsync(IDictionary<string, IEnumerable<object>> events);
– A method that saves the GIVEN events (events that have already occurred) to a location where they can be read by the Event Sourcing framework of your choice to rehydrate domain objects (e.g., aggregates in DDD). This can be a fake in-memory implementation of anIEventDatabase
interface or a similiar interface used by your framework of choice.Task<IDictionary<string, IEnumerable<object>>> GetPublishedEventsAsync();
– In every event sourcing framework, there is a component responsible for saving newly published events. Implement this method so that EventOutcomes can retrieve those newly published events.Task DispatchCommandAsync(object command);
– Place your command dispatching code here. For example:await _commandDispatcher.Dispatch(command);
orawait _massTransitMediator.Publish(command);
.
Below is an example of how IAdapter
can be implemented.
public class MyAdapter : IAdapter
{
public MyAdapter()
{
var services = new ServiceCollection();
services.AddScoped<IEventDatabase, FakeEventDatabase>();
// register all other services here - your main application registration code and all other fakes used for your tests
ServiceProvider = services.BuildServiceProvider();
}
public IServiceProvider ServiceProvider { get; private set; }
public Task BeforeTestAsync()
{
ServiceProvider = ServiceProvider.CreateScope().ServiceProvider;
return Task.CompletedTask;
}
public Task AfterTestAsync()
{
return Task.CompletedTask;
}
public Task SetGivenEventsAsync(IDictionary<string, IEnumerable<object>> events)
{
var fakeEventDatabase = ServiceProvider.GetRequiredService<IEventDatabase>() as FakeEventDatabase;
fakeEventDatabase.StubAlreadySavedEvents(events);
return Task.CompletedTask;
}
public Task<IDictionary<string, IEnumerable<object>>> GetPublishedEventsAsync()
{
var fakeEventDatabase = ServiceProvider.GetRequiredService<IEventDatabase>() as FakeEventDatabase;
return Task.FromResult(fakeEventDatabase.GetNewlySavedEvents());
}
public async Task DispatchCommandAsync(object command)
{
var massTransitMediator = ServiceProvider.GetRequiredService<IMediator>();
await massTransitMediator.Publish(command);
}
}
It is a good idea to use the DRY principle and write our own wrapper for the Tester class:
public class MyCustomTesterWrapper
{
public async Task TestAsync(Test test) => await Tester.TestAsync(test, new MyAdapter());
}
Thanks to this, we can execute our test from above by writing:
[Fact]
public async Task given_bread_ingredients_mixed_when_BakeDough_for_25_minutes_then_bread_baked_and_fantastic_smell_produced()
{
// ...
await MyCustomTesterWrapper.TestAsync(test);
}
The Given
method can be called multiple times on a single instance of the Test
class.
test.Given(new EventA()).Given(new EventB());
is equivalent to:
test.Given(new EventA(), new EventB());
There are a few other options to arranging our tests and declaring what has already happend.
To declare what events have occurred, use:
.Given(new FirstEvent(), new SecondEvent(), new ThirdEvent() /*, ...*/)
To call any action on any service, use:
.Given<IWeatherService>(s => s.ConfigurePressureUnit(PressureUnit.Hectopascal))
To call any action on any fake service, use:
.Given<IGeoLocationService, FakeGeoLocationService>(s => s.StubLocation(53, Latitude.North, 18, Longitude.East))
To specify a command that will be dispatched to your application code, use the When
method.
To assert, use the Then
method, which has many variations. All of them are described below:
ThenNone()
– the test passes if no event was published and no exception was thrown.ThenAny()
– test passes if any events occurred or if no events occurred. This method only makes sense when it is combined with otherThen
methods. For example, if we want to check ifFirstEventoccurred
andLastEventoccurred
occurred, but we don't care about any events that may have occurred in between, then we can write:.Then(new FirstEventOccurred()) .ThenAny() .Then(new LastEventOccurred())
ThenNot(params Func<object, bool>[] excludedEventQualifiers)
– the test passes if none of the events that occurred matches any of theexcludedEventQualifiers
. For example:.ThenNot( e => e is FirstEventOccurred { V: 999, }, e => e is SecondEventOccurred { V: "x", })
Then(object expectedEvent)
– the test passes if exactly one event occurred and that event is the same as the event specified in theThen
method.ThenInOrder(params object[] expectedEvents)
– the test passes if the same events occurred in the specified order.ThenInAnyOrder(params object[] expectedEvents)
– the test passes if the same events occurred in any order.Then<TService>(Func<TService, AssertActionResult> assertAction)
– the test passes if the assertion action returnstrue
orAssertActionResult.Successful()
. There is also an async version of this method.Then<TService, TFakeService>(Func<TFakeService, AssertActionResult> assertAction)
– the test passes if the assertion action returnstrue
orAssertActionResult.Successful()
. There is also an async version of this method.Then(Func<IServiceProvider, AssertActionResult> assertAction)
– the test passes if the assertion action returnstrue
orAssertActionResult.Successful()
. There is also an async version of this method.ThenException(params IExceptionAssertion[] exceptionAssertions)
– the test passes if an exception was thrown. There are two built-in implementations ofIExceptionAssertion
–ExceptionTypeAssertion
andExceptionMessageAssertion
– but you can write your own implementations.ThenException<TExpectedException>(string expectedMessage, ExceptionMessageAssertionType matchingType)
– the test passes if an exception of the specified type and with the specified message was thrown.ThenAnyException<TExpectedException>(string expectedMessage, ExceptionMessageAssertionType matchingType)
– the test passes if an exception of the specified type or a derived type and with the specified message was thrown.
The domain logic you are writing tests for may depend on the state of multiple aggregates. EventOutcomes provides a way to use Given
methods for a specified event stream id:
var test = Test.ForMany()
.Given(firstEventStreamId, new SomeEvent(firstEventStreamId, "some event data"))
.Given(secondEventStreamId, new SomeEvent(secondEventStreamId, "some event data"))
instead of the standard single-stream method:
var test = Test.For(eventStreamId)
.Given(new SomeEvent(eventStreamId, "some event data"))
Altough this is generally bad idea to save two streams of events within single operation, EventOutcomes provides a way to write unit tests for such cases. To do this, you should use Then
methods for a specified event stream id:
var test = Test.ForMany()
.Given(firstEventStreamId, new SomeEvent(firstEventStreamId, "some event data"))
.Given(secondEventStreamId, new SomeEvent(secondEventStreamId, "some event data"))
.When(new DoSomethingCommand())
.Then(firstEventStreamId, new SomeOtherEvent(firstEventStreamId, "some other event data"))
.Then(secondEventStreamId, new SomeOtherEvent(secondEventStreamId, "some other event data"))
instead of the standard single-stream method:
var test = Test.For(eventStreamId)
.Given(new SomeEvent(eventStreamId, "some event data"))
.When(new DoSomethingCommand())
.Given(new SomeOtherEvent(eventStreamId, "some other event data"))
An example application can be found here.