Fiffi
Exploration kit for event driven services. Fiffi is a framework for code feedback of modeling. It forces some contracts on your infrastructure AND domain.
Core is the Fiffi IEvent envelope, EventRecord, ICommand, IQuery. Infrastructure is IAdvancedEventStore and ISnapshopStore.
Testing
βββββββββββββββββββ³ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ³βββββββ³βββββββββββββββββββ β Flow β Waterfall β Time β Aggregate β β£ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ« β FooCommand β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 0 β 99a9c841-ebd9-4d β β FooEvent β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 1 β 99a9c841-ebd9-4d β β FooEvent β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 1 β 4cbcde1d-f12d-42 β β BarCommand β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 2 β 5800daa3-a109-4d β β BarEvent β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 3 β 5800daa3-a109-4d β β BarEvent β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 3 β a5c155cb-3f2d-4a β β BarCommand β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 2 β f20ba848-44f1-45 β β BarEvent β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 3 β f20ba848-44f1-45 β β BarEvent β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 3 β 21e1360f-13af-45 β βββββββββββββββββββ»ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ»βββββββ»βββββββββββββββββββ
[Fact]
public async Task CreateGame()
{
//Given
context.Given(Array.Empty<IEvent>());
//When
await context.WhenAsync(new CreateGame { FirstTo = 3, GameId = Guid.NewGuid(), PlayerId = "tester", Title = "Test Game" });
//Then
context.Then((events, visual) => {
this.helper.WriteLine(visual);
Assert.True(events.OfType<GameCreated>().Happened());
});
}
At it's core is an definition of an IEventStore and the IEvent.
public interface IEventStore
{
Task<long> AppendToStreamAsync(string streamName, long version, IEvent[] events);
Task<(IEnumerable<IEvent> Events, long Version)> LoadEventStreamAsync(string streamName, int version);
}
The kit then focuses on creating a module of use cases with event driven state and flows.
The ApplicationService.ExecuteAsync is targeted on the infrastructure part of an application service.
ApplicationService.ExecuteAsync(
store,
command,
state => Array.Empty<IEvent>(),
events => pub(events)
)
The core Fiffi project also includes tools for handling command, query, event dispatching/routing, meta data and concurrency options.
The vision is to create a kit to get feedback/test from modeling/event storming sessions.
Modules
Using the Fiffi TestContext is based on using Modules. A boundary with a facade accepting commands, events and queries, it also publishes events - a key part of testing.
Testing With Modules
The first option would be using the configuration builder.
public class TestModule : Module
{
public TestModule(ModuleCore core) : base(core)
{ }
public static TestModule Initialize(IAdvancedEventStore store, ISnapshotStore snapshotStore, Func<IEvent[], Task> pub)
=> new Configuration<TestModule>(x => new TestModule(x))
.Commands(cmd => store.ExecuteAsync(cmd, "test", () => new[] { new TestEventRecord("test") }, pub))
.Updates(events => snapshotStore.Apply("test", new TestView(0), events.Select(x => x.Event), Apply))
.Triggers((events, dispatcher) => Task.CompletedTask)
.Query<TestQuery, TestView?>(q => snapshotStore.Get<TestView>("test"))
.Create(store);
public record TestView(int EventCount) : View;
public static TestView Apply(
TestView snap,
EventRecord @event) => snap with { EventCount = snap.EventCount + 1 };
public record TestQuery() : IQuery<TestView?>;
}
Creating a TestContext for integration tests using test extension
Using the TestModule we use the TestHost and TextContext, constructed by an extension.
(host, context) = Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(webBuilder =>
webBuilder.ConfigureServices(services =>
services
.AddFiffiInMemory()
.AddModule(TestModule.Initialize)
.AddInMemoryEventSubscribers()
)
.Configure(app => app.UseWelcomePage()))
.CreateFiffiTestContext(TestModule.Initialize);
In a "real" scenario the services configuration would be in Program or equivalent, so we could user the IHostBuilder. The replacement of Fiffi infrastructure would also be done via test services.
.CreateFiffiTestContext(
testServices => testServices.AddFiffiInMemory(),
TestModule.Initialize);
The Fiffi CreateFiffiTestContext replaces the subject module and event subscribers (with a test spy and additional test modules).
Armed with the host and context we could start authoring a simple test.
[Fact]
public async Task SimpleEventCaptureAsync()
{
await host.StartAsync();
await context.WhenAsync(new TestCommand(new AggregateId("test")));
context.Then((events, visual) => Assert.True(events.Happened<TestEventRecord>()));
await context.ThenAsync(new TestModule.TestQuery(), result => Assert.Equal(1, result.EventCount));
}
The TestContext offers a Given, When, Then (GWT) approach to interact with the subject module (and additional modules). A visualization extension also offers a ascii timeline, that you could output via your test framework.
The GWT approach also lets us build a current state by a history of events, by building event envelopes for testing.
[Fact]
public async Task HistoryEventCaptureAsync()
{
await host.StartAsync();
context.Given(EventEnvelope //todo extension
.Create("test", new TestEventRecord("test"))
.AddTestMetaData("test"));
await context.WhenAsync(new TestCommand(new AggregateId("test")));
context.Then((events, visual) => Assert.True(events.Happened<TestEventRecord>()));
await context.ThenAsync(new TestModule.TestQuery(), result => Assert.Equal(2, result.EventCount));
}
The TestContext will apply the history of events to the configured event store and projections, based on stream meta data and module configuration.
Creating a TestContext for integration tests using context builder
If you don't need/want easy access to the ServiceProvider you could initiate the Host within the Creation of the subject module, illustrated by the RPS sample.
this.context = TestContextBuilder.Create<InMemoryEventStore, GameModule>((store, pub) =>
{
this.helper = outputHelper;
var module = GameModule.Initialize(store, new InMemorySnapshotStore(), pub);
hostBuilder = RPS.Web.Program.CreateHostBuilder(Array.Empty<string>())
.ConfigureWebHost(webHost => webHost
.UseTestServer()
.ConfigureTestServices(services =>
services
.AddSingleton(module)
.Remove(services.SingleOrDefault(x => x.ImplementationType == typeof(ChangeFeedHostedService)))
));
return module;
});
Alternative way of authoring modules
An alternative to the more functional builder describe above, is to implement the IModule interface.
public class TestInterfaceModule : IModule
{
readonly IAdvancedEventStore store;
readonly ISnapshotStore snapshotStore;
readonly Func<IEvent[], Task> pub;
public TestInterfaceModule(IAdvancedEventStore store, ISnapshotStore snapshotStore, Func<IEvent[], Task> pub)
{
this.store = store;
this.snapshotStore = snapshotStore;
this.pub = pub;
}
public Task DispatchAsync(ICommand command) =>
store.ExecuteAsync(command, "test", () => new[] { new TestEventRecord("test") }, pub);
public Task OnStart(IEvent[] events) => WhenAsync(events);
public async Task<T?> QueryAsync<T>(IQuery<T> q) where T : class
=> await (q switch
{
TestQuery => snapshotStore.Get<T>("test"),
_ => throw new InvalidOperationException()
});
public IAsyncEnumerable<T> QueryAsync<T>(IStreamQuery<T> q)
=> throw new NotImplementedException();
public Task WhenAsync(params IEvent[] events) =>
snapshotStore.Apply("test", new TestView(0), events.Select(x => x.Event), Apply);
}
Note that there is no Update or Trigger methods in IModule. In the builder version a few thing are done under the hood, to aid startup and run updates before triggers. In the IModule version you handle OnStart and WhenAsync your self.