
Before Marten took off and we pivoted to using the “Critter Stack” naming motif, the original naming theme for the JasperFx OSS tool suite were some of the small towns near where I grew up in Southwest Missouri. Alba, MO is somewhat famous as the hometown of the Boyer brothers.
I’m taking a little time this week to build out some improvements to Wolverine’s declarative data access support based on some recent client work. As this work is largely targeted at Wolverine’s HTTP support, I’m heavily leveraging Alba to help test the HTTP behavior and I thought this work would make a great example of how Alba can help you more efficiently test HTTP API code in .NET.
Now, back to Wolverine and the current work I’m in the midst of testing today. To remove a lot of the repetitive code out of this client’s HTTP API, Wolverine is going to improve the [Entity] attribute mechanics to easily customize “on missing” handling something like this simple example from tests:
// Should 400 w/ ProblemDetails on missing
[WolverineGet("/required/todo400/{id}")]
public static Todo2 Get2([Entity(OnMissing = OnMissing.ProblemDetailsWith400)] Todo2 todo)
=> todo;
With Wolverine message handlers or HTTP endpoints, the [Entity] attribute is a little bit of declarative data access that just directs Wolverine to generate some code around your method to load data for that parameter based on its type from whatever your attached data access tooling is for that application, currently supported for Marten (of course), EF Core, and RavenDb. In its current form, if Marten/EF Core/RavenDb cannot find a Todo2 entity in the database with the identity from the route argument “id”, Wolverine will just set the HTTP status code to 404 and exit.
And while I’d argue that’s a perfectly fine default behavior, a recent client wants instead to write out a ProblemDetails response describing what data referenced in the request was unavailable and return a 400 status code instead. They’re handling that with Wolverine’s Railway Programming support just fine, but I think that’s causing my client more repetitive code than I personally prefer, and Wolverine is based on the philosophy that repetitive code should be minimized as much as possible. Hence, the enhancement work hinted at above with a new OnMissing property that lets you specify exactly how an HTTP endpoint should handle the case where a requested entity is missing.
So let’s finally introduce Alba with this test harness using xUnit:
public class reacting_to_entity_attributes : IAsyncLifetime
{
private readonly ITestOutputHelper _output;
private IAlbaHost theHost;
public reacting_to_entity_attributes(ITestOutputHelper output)
{
_output = output;
}
public async Task InitializeAsync()
{
// This probably isn't your typical Alba usage, but
// I'm spinning up a little AspNetCore application
// for endpoint types in the current testing assembly
var builder = WebApplication.CreateBuilder([]);
// Adding Marten as the target persistence provider,
// but the attribute does work w/ EF Core too
builder.Services.AddMarten(opts =>
{
// Establish the connection string to your Marten database
opts.Connection(Servers.PostgresConnectionString);
opts.DatabaseSchemaName = "onmissing";
}).IntegrateWithWolverine().UseLightweightSessions();
builder.Host.UseWolverine(opts => opts.Discovery.IncludeAssembly(GetType().Assembly));
builder.Services.AddWolverineHttp();
// This is using Alba, which uses WebApplicationFactory under the covers
theHost = await AlbaHost.For(builder, app =>
{
app.MapWolverineEndpoints();
});
}
async Task IAsyncLifetime.DisposeAsync()
{
if (theHost != null)
{
await theHost.StopAsync();
}
}
// Other tests...
[Fact]
public async Task problem_details_400_on_missing()
{
var results = await theHost.Scenario(x =>
{
x.Get.Url("/required/todo400/nonexistent");
x.StatusCodeShouldBe(400);
x.ContentTypeShouldBe("application/problem+json");
});
var details = results.ReadAsJson<ProblemDetails>();
details.Detail.ShouldBe("Unknown Todo2 with identity nonexistent");
}
}
Just a few things to call out about the test above:
- Alba is using WebApplicationFactory and TestServer from AspNetCore under the covers to bootstrap an AspNetCore
IHostwithout having to use Kestral - The Alba
Scenario()method is running an HTTP request all the way through the application in process - Alba has declarative helpers to assert on the expected HTTP status code and content-type headers in the response, and I used those above
- The
ReadAsJson<T>()helper just helps us deserialize the response body into a .NET type using whatever the JSON serialization configuration is within our application — and by no means should you minimize that because that’s a humongous potential source of false test results for the unwary if folks use mismatched JSON serialization settings between their application and test harness code!
For the record, that test is passing in my local branch right now after a couple iterations. Alba just happened to make the functionality pretty easy to test through both the declarative assertions and the JSON serialization helpers.