I'm looking for a way to test functionality written using Marten. Not just as a unit test, but where integration with asp.net API's and actual saving to the database is tested. Moreover, I want to test whether async projections are generated as well. So to sum up, I would like to test whether the following is executed correctly:
- Domain Driven Design styled command is received by a controller action
- The command is translated into a domain event and that event has been saved to PostgreSQL
- After a while a projection is asynchronously generated and saved to PostgreSQL as well
Marten does not provide concrete examples in their documentation. They do have unit-test in their codebase, but these are not standalone enough to provide enough information to build your own integration tests in my opinion.
Given the following setup (inspired by Marten examples):
Command
public record StartQuest(
Guid Id,
string Name,
int Day,
string Location,
params string[] Members) : IRequest;
Aggregate, projection and events
public class Quest
{
public Guid Id { get; set; }
}
public class QuestPartyProjection : SingleStreamAggregation<QuestParty>
{
public QuestParty Create(QuestStarted @event) => new() { Name = @event.Name };
public void Apply(QuestParty view, MembersJoined @event) => view.Members.Fill(@event.Members);
}
public class QuestParty
{
public Guid Id { get; set; }
public List<string> Members { get; set; } = new();
public string Name { get; set; }
}
public record MembersJoined(int Day, string Location, string[] Members);
public record QuestStarted(string Name);
MediatR is used to delegate commands to command handlers
[ApiController]
[Route("[controller]")]
public class QuestController : ControllerBase
{
private readonly IMediator mediator;
public QuestController(IMediator mediator)
{
this.mediator = mediator;
}
[HttpPost("[Action]")]
public async Task<IActionResult> Start([FromBody] StartQuest command, CancellationToken cancellationToken)
{
await mediator.Send(command, cancellationToken);
return Ok();
}
}
internal sealed class StartHandler : IRequestHandler<StartQuest>
{
private readonly IDocumentSession session;
public StartHandler(IDocumentSession session)
{
this.session = session;
}
public async Task<Unit> Handle(StartQuest command, CancellationToken cancellationToken)
{
var started = new QuestStarted(command.Name);
var joined1 = new MembersJoined(command.Day, command.Location, command.Members);
session.Events.StartStream(typeof(Quest), command.Id, started, joined1);
await session.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}
Which involves the following setup in my .net application:
builder.Services.AddMarten(x =>
{
x.Connection(builder.Configuration.GetConnectionString("Marten")!);
x.Projections.Add<QuestPartyProjection>(ProjectionLifecycle.Async);
})
.OptimizeArtifactWorkflow(TypeLoadMode.Static)
.AddAsyncDaemon(DaemonMode.HotCold)
.UseLightweightSessions()
.InitializeWith();;
builder.Services.AddMediatR(typeof(StartHandler));
UPDATE 2023-12-10: I wrote documentation for Marten which gets updated and works better here: Integration testing - Marten
I created integration teest using xunit (for declaring and executing tests), TestContainers (for running a Docker PostgreSQL container o,n the fly), and Microsoft.AspNetCore.Mvc.Testing for bootstrapping the application in memory for functional end-to-end tests.
My csproj has these package references (and a reference to the project to be tested):
First, since mvc.testing need a public entrypoint, I've added this to the program.cs
Now in the following code a custom WebApplicationFactory is created on top of Microsoft's end to end testing framework. A new docker instance for PostgreSQL is created for every run and the application configuration is overwritten by the connectionstring that points to that PostgreSQL instance.
I created an abstract integration class for reuse:
And now my actual unit test is pretty straightforward: