Skip to content

Commit 59956c8

Browse files
author
Andreas Gehrke
committed
Functional tests of Command execution and projections
1 parent 8142798 commit 59956c8

8 files changed

+560
-1
lines changed

test/Atc.Cosmos.EventStore.Cqrs.Tests/Atc.Cosmos.EventStore.Cqrs.Tests.csproj

+5
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
<PropertyGroup>
44
<TargetFramework>net9.0</TargetFramework>
55
<IsPackable>false</IsPackable>
6+
67
</PropertyGroup>
78

89
<ItemGroup>
910
<PackageReference Include="Atc.Test" Version="1.1.4" />
1011
<PackageReference Include="FluentAssertions" Version="6.12.1" />
12+
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="9.0.3" />
1113
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
1214
<PackageReference Include="xunit" Version="2.9.2" />
1315
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
@@ -18,6 +20,9 @@
1820
<PrivateAssets>all</PrivateAssets>
1921
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2022
</PackageReference>
23+
24+
<!-- Get the shared Microsoft.AspNetCore.App framework instead of referencing a bunch of MS.Ext.Hosting etc packages -->
25+
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
2126
</ItemGroup>
2227

2328
<ItemGroup>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Xunit;
3+
4+
namespace Atc.Cosmos.EventStore.Cqrs.Tests.Functional;
5+
6+
#nullable enable
7+
8+
[Trait("Category", "Functional")]
9+
public class CommandHandlerTests : IAsyncLifetime
10+
{
11+
private CqrsTestHost host = null!;
12+
13+
[Fact]
14+
public async Task Result_from_CommandHandler_must_be_returned()
15+
{
16+
var tick = DateTime.UtcNow.Ticks;
17+
var commandResult = await host.Services.GetRequiredService<ICommandProcessorFactory>()
18+
.Create<MakeTimeTickCommand>()
19+
.ExecuteAsync(new MakeTimeTickCommand(tick), CancellationToken.None);
20+
21+
Assert.NotNull(commandResult);
22+
Assert.NotNull(commandResult.Response);
23+
Assert.Equal(tick, commandResult.Response);
24+
}
25+
26+
[Fact]
27+
public async Task CommandHandler_can_consume_existing_events()
28+
{
29+
// Produce some events by making time tick
30+
await MakeTimeTick(host);
31+
await MakeTimeTick(host);
32+
await MakeTimeTick(host);
33+
34+
// Query events
35+
var result = await host.Services.GetRequiredService<ICommandProcessorFactory>()
36+
.Create<QueryTimeTickCommand>()
37+
.ExecuteAsync(new QueryTimeTickCommand(), CancellationToken.None);
38+
39+
var events = Assert.IsType<List<(TimeTickedEvent Evt, EventMetadata Metadata)>>(result.Response);
40+
Assert.Equal(3, events.Count);
41+
42+
static async Task MakeTimeTick(CqrsTestHost host)
43+
{
44+
var tick = DateTime.UtcNow.Ticks;
45+
var commandProcessorFactory = host.Services.GetRequiredService<ICommandProcessorFactory>();
46+
_ = await commandProcessorFactory
47+
.Create<MakeTimeTickCommand>()
48+
.ExecuteAsync(new MakeTimeTickCommand(tick), CancellationToken.None);
49+
}
50+
}
51+
52+
[Fact]
53+
public async Task CommandHandler_that_consumes_events_works_when_no_existing_events_are_present()
54+
{
55+
// Query events - none exists
56+
var result = await host.Services.GetRequiredService<ICommandProcessorFactory>()
57+
.Create<QueryTimeTickCommand>()
58+
.ExecuteAsync(new QueryTimeTickCommand(), CancellationToken.None);
59+
60+
var events = Assert.IsType<List<(TimeTickedEvent Evt, EventMetadata Metadata)>>(result.Response);
61+
Assert.Empty(events);
62+
}
63+
64+
[Fact]
65+
public async Task Command_that_uses_RequiredVersion_Exists_must_result_in_NotFound_when_no_existing_events_are_present()
66+
{
67+
// Query events - must fail as non exists
68+
var result = await host.Services.GetRequiredService<ICommandProcessorFactory>()
69+
.Create<QueryExistingTimeTickCommand>()
70+
.ExecuteAsync(new QueryExistingTimeTickCommand(), CancellationToken.None);
71+
72+
Assert.Equal(ResultType.NotFound, result.Result);
73+
}
74+
75+
public async Task InitializeAsync()
76+
{
77+
host = new CqrsTestHost();
78+
await host.StartAsync();
79+
}
80+
81+
public async Task DisposeAsync()
82+
{
83+
await host.DisposeAsync();
84+
}
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using Atc.Cosmos.EventStore.Streams;
2+
using Microsoft.AspNetCore.Builder;
3+
using Microsoft.AspNetCore.TestHost;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.DependencyInjection.Extensions;
6+
7+
namespace Atc.Cosmos.EventStore.Cqrs.Tests.Functional;
8+
9+
#nullable enable
10+
11+
public class CqrsTestHost : IAsyncDisposable
12+
{
13+
private readonly WebApplication host;
14+
15+
public CqrsTestHost()
16+
{
17+
host = CreateHostBuilder().Build();
18+
}
19+
20+
public IServiceProvider Services => host.Services;
21+
22+
public async Task StartAsync()
23+
{
24+
await host.StartAsync();
25+
}
26+
27+
public async Task StopAsync()
28+
{
29+
await host.StopAsync();
30+
}
31+
32+
public async ValueTask DisposeAsync()
33+
{
34+
await host.StopAsync();
35+
await host.DisposeAsync();
36+
}
37+
38+
private static WebApplicationBuilder CreateHostBuilder()
39+
{
40+
// Build a host with Atc EventStore
41+
var webApplicationBuilder = WebApplication.CreateEmptyBuilder(new WebApplicationOptions()
42+
{
43+
ApplicationName = "Atc.Cosmos.EventStore.Cqrs.Tests",
44+
});
45+
46+
webApplicationBuilder.WebHost.UseTestServer();
47+
48+
// Configure EventStore
49+
webApplicationBuilder.Services.AddEventStore(eventStoreBuilder =>
50+
{
51+
eventStoreBuilder.UseEvents(c => c.FromAssembly<CqrsTestHost>());
52+
eventStoreBuilder.UseCQRS(c =>
53+
{
54+
c.AddCommandsFromAssembly<CqrsTestHost>();
55+
c.AddProjectionJob<TimeProjection>("TimeProjection");
56+
});
57+
});
58+
59+
// Use InMemoryEventStoreClient which actually works
60+
webApplicationBuilder.Services.Replace(
61+
ServiceDescriptor.Singleton<IEventStoreClient, InMemoryEventStoreClient>());
62+
63+
// Remove unused registrations
64+
webApplicationBuilder.Services.RemoveAll<IStreamInfoReader>();
65+
webApplicationBuilder.Services.RemoveAll<IStreamMetadataReader>();
66+
webApplicationBuilder.Services.RemoveAll<IStreamReader>();
67+
webApplicationBuilder.Services.RemoveAll<IStreamWriter>();
68+
69+
webApplicationBuilder.Services.AddSingleton<FakeDatabase>();
70+
71+
return webApplicationBuilder;
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#nullable enable
2+
namespace Atc.Cosmos.EventStore.Cqrs.Tests.Functional;
3+
4+
internal class FakeDatabase
5+
{
6+
private readonly Dictionary<string, object> storage = new();
7+
8+
public void Save(string key, object value)
9+
{
10+
storage[key] = value;
11+
}
12+
13+
public object? Load(string key)
14+
{
15+
storage.TryGetValue(key, out var value);
16+
return value;
17+
}
18+
}

0 commit comments

Comments
 (0)