Fluent Integration Testing

Fluent Integration Testing

Microsoft have a decent, open source integration test library for MVC on ASP.NET Core 2.1, with some pretty in depth documentation. In true post evil Microsoft form, this package provides the bare minimum, lacking opinionated views on structure or choice of testing framework. This is awesome if like me you develop integration tests with a pretty strong opinion. My (current) opinion in this regard is based around a fluent style - I like my tests to be self documenting and built from reusable components. To demonstrate, I have created xunit-fixture-mvc, which can be found on GitHub and NuGet.

Integration tests are all about testing two or more components of an application together. We tend not to write many integration tests as they are difficult to develop and even more difficult to maintain, whilst also being slow to run. We can get away with this for integration testing as integration tests should be only part of the full test stack. Along with integration tests, we should also have a larger, faster suite of unit tests and if we have a UI, a smaller, slower suite of UI driven end-to-end tests.

I think that a good integration test is:

ASP.NET Core has a hosting provider specifically for integration testing: Microsoft.AspNetCore.TestHost. This replaces the bit of ASP.NET Core that handles HTTP requests, usually Kestrel (or IIS if you're using in-process hosting and are crazy), with something that handles simulated HTTP requests from our integration tests.

Ok so this isn't a true integration test then as Kestrel isn't handling the HTTP requests right?

Yes, that's true. By ripping out Kestrel and replacing it with something that's more convenient for testing, we're not testing the same ASP.NET Core stack that we will deploy to production. I think that we can get away with it here though as the convenience of using the test server far outweighs the drawbacks of some exotic multi-process solution. Consider such a multi-process solution would look like. Initially, we would probably rely on docker, which sounds promising. However, our test container wouldn't have direct control over the lifetime of an application container so in order for testing to be isolated, each integration test would have to spin up it's own copy of the application container. That would be a lot of containers and a massive scripting headache. Also, how would each integration test know when each application container is ready? The only viable option is to poll it, which is very inefficient and messy.

One major thing we cannot ignore when accepting the use of the test server is that we're no longer testing whether the application actually works inside of a container. For example, if you were to upgrade the ASP.NET Core packages but not the base image in your Dockerfile, then the application would build fine and all integration tests might pass but we'd end up with an application image that cannot be started. The solution to this is to write a "health check" type test that would spin up an application container and all of it's dependencies then make a very simple request.

Here's the example ripped from Microsoft's MVC integration test documentation:

public class BasicTests
: IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
private readonly WebApplicationFactory<RazorPagesProject.Startup> _factory;

public BasicTests(WebApplicationFactory<RazorPagesProject.Startup> factory)
{
_factory = factory;
}

[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();

// Act
var response = await client.GetAsync(url);

// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
public class BasicTests
: IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
private readonly WebApplicationFactory<RazorPagesProject.Startup> _factory;

public BasicTests(WebApplicationFactory<RazorPagesProject.Startup> factory)
{
_factory = factory;
}

[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();

// Act
var response = await client.GetAsync(url);

// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}

Other than this being a test for a dirty, dirty Razor Page's project I have some issues with it.

Imagine we had a simple API that returns the current UTC datetime.

[Route("date")]
public class DateController : Controller
{
[HttpGet]
public DateDto Get() => new DateDto
{
UtcNow = DateTimeOffset.UtcNow
};
}
[Route("date")]
public class DateController : Controller
{
[HttpGet]
public DateDto Get() => new DateDto
{
UtcNow = DateTimeOffset.UtcNow
};
}

A fluent test for this developed with xunit-fixture-mvc might look like this:

public class DateTests
{
private readonly ITestOutputHelper _output;

public DateTests(ITestOutputHelper output)
{
_output = output;
}

[Fact]
public Task When_getting_date() =>
new MvcFunctionalTestFixture<Startup>(_output)
.WhenGetting("date")
.ShouldReturnSuccessfulStatus()
.ShouldReturnJson<DateDto>(x => x.UtcNow.Should().BeCloseTo(DateTimeOffset.UtcNow, 1000))
.RunAsync();
}
public class DateTests
{
private readonly ITestOutputHelper _output;

public DateTests(ITestOutputHelper output)
{
_output = output;
}

[Fact]
public Task When_getting_date() =>
new MvcFunctionalTestFixture<Startup>(_output)
.WhenGetting("date")
.ShouldReturnSuccessfulStatus()
.ShouldReturnJson<DateDto>(x => x.UtcNow.Should().BeCloseTo(DateTimeOffset.UtcNow, 1000))
.RunAsync();
}

This already reads pretty well but let's break it down:

new MvcFunctionalTestFixture<Startup>(_output)
new MvcFunctionalTestFixture<Startup>(_output)

Configures the test server to use the specified Startup class and to send all logs to the xunit test context.

.WhenGetting("date")
.WhenGetting("date")

Configures the fixture to send a GET /date request to the test server once it's up and running.

.ShouldReturnSuccessfulStatus()
.ShouldReturnSuccessfulStatus()

Adds an assertion to the fixture that the response has a successful (2xx) code.

.ShouldReturnJson<DateDto>(x => x.UtcNow.Should().BeCloseTo(DateTimeOffset.UtcNow, 1000))
.ShouldReturnJson<DateDto>(x => x.UtcNow.Should().BeCloseTo(DateTimeOffset.UtcNow, 1000))

Adds an assertion to the fixture that the response body can be deserialized to an instance of DateDto and that the UtcNow property has been set correctly. We're using the excellent Fluent Assertions.

.RunAsync();
.RunAsync();

Runs the fixture. It's important to note that all preceding fluent calls are simply configuration steps and only here do we actually construct a test server. In this case the configured fixture will:

  1. Create a new test server, bootstrapped with the API Startup class.
  2. Send a GET /date request to the API.
  3. Assert that the response has a successful (2xx) code.
  4. Attempt to deserialize the response body and run assertions on the deserialized object.
  5. If no exceptions are thrown, do nothing and let the test pass. Otherwise:
    • If one exception is thrown => re-throw the exception.
    • If multiple exceptions are thrown => throw an aggregate exception containing all thrown exceptions.

A conscious design decision was that no exception will be thrown until all assertions have been run. The reason why I prefer this approach is to avoid cases where a test is failing for multiple reasons yet it's error message does not reflect this. It can be a labouring experience to fix a broken test, one failure at a time.

I like to develop these fluent fixture type patterns with extension methods. A decent example in xunit-fixture-mvc is Xunit.Fixture.Mvc.Extensions.RestExtensions, which uses a RESTful opinion to call the When method on Xunit.Fixture.Mvc.IMvcFunctionalTestFixture. The example above already used one of these extensions:

/// <summary>
/// Configures the specified fixture's act step to be a GET request at the specified url.
/// </summary>
public static IMvcFunctionalTestFixture WhenGetting(this IMvcFunctionalTestFixture fixture, string url) =>
fixture.WhenCallingRestMethod(HttpMethod.Get, url);


/// <summary>
/// Configures the specified fixture's act step to be an HTTP request with the specified method, url and body.
/// </summary>
public static IMvcFunctionalTestFixture WhenCallingRestMethod(this IMvcFunctionalTestFixture fixture,
HttpMethod method, string url, object body = null)
{
var content = body == null
? null :
new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");

return fixture.When(method, url, content);
}
/// <summary>
/// Configures the specified fixture's act step to be a GET request at the specified url.
/// </summary>
public static IMvcFunctionalTestFixture WhenGetting(this IMvcFunctionalTestFixture fixture, string url) =>
fixture.WhenCallingRestMethod(HttpMethod.Get, url);


/// <summary>
/// Configures the specified fixture's act step to be an HTTP request with the specified method, url and body.
/// </summary>
public static IMvcFunctionalTestFixture WhenCallingRestMethod(this IMvcFunctionalTestFixture fixture,
HttpMethod method, string url, object body = null)
{
var content = body == null
? null :
new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");

return fixture.When(method, url, content);
}

The alternative to this would have been to use inheritance to add methods to the fixture, which would mess with the ability for tests to be written in a fluent style. I.e. if any method was called that returns an IMvcFunctionalTestFixture, all instance methods would be inaccessible to proceeding, chained calls.

The use of extensions methods can also extend to the domain of the application that we're testing. For example, there may be more than one API endpoint that returns a DateDto. If that's the case then it would make sense to spin this off into a reusable, fluent extension method.

public static class DateAssertionExtensions
{
public static IMvcFunctionalTestFixture ShouldReturnDateCloseToNow(this IMvcFunctionalTestFixture fixture) =>
fixture.ShouldReturnJson<DateDto>(x => x.UtcNow.Should().BeCloseTo(DateTimeOffset.UtcNow, 1000));
}
public static class DateAssertionExtensions
{
public static IMvcFunctionalTestFixture ShouldReturnDateCloseToNow(this IMvcFunctionalTestFixture fixture) =>
fixture.ShouldReturnJson<DateDto>(x => x.UtcNow.Should().BeCloseTo(DateTimeOffset.UtcNow, 1000));
}

The fluent design of our integration test can now be cleaned up further.

[Fact]
public Task When_getting_date() =>
new MvcFunctionalTestFixture<Startup>(_output)
.WhenGetting("date")
.ShouldReturnSuccessfulStatus()
.ShouldReturnDateCloseToNow()
.RunAsync();
[Fact]
public Task When_getting_date() =>
new MvcFunctionalTestFixture<Startup>(_output)
.WhenGetting("date")
.ShouldReturnSuccessfulStatus()
.ShouldReturnDateCloseToNow()
.RunAsync();

For generating request objects, xunit-fixture-mvc uses AutoFixture. Take a look at these methods from Xunit.Fixture.Mvc.Extensions.RestExtensions.

/// <summary>
/// Configures the specified fixture's act step to be a PUT request for the specified entity and id with the specified JSON body.
/// </summary>
public static IMvcFunctionalTestFixture WhenUpdating<TId, TModel>(this IMvcFunctionalTestFixture fixture,
string entity, TId id, out TModel model) =>
fixture.WhenCallingRestMethod(HttpMethod.Put, $"{entity}/{Uri.EscapeDataString(id.ToString())}", out model);

/// <summary>
/// Configures the specified fixture's act step to be an HTTP request with the specified method, url and body.
/// </summary>
public static IMvcFunctionalTestFixture WhenCallingRestMethod<TModel>(this IMvcFunctionalTestFixture fixture,
HttpMethod method, string url, out TModel model) =>
fixture.HavingModel(out model)
.WhenCallingRestMethod(method, url, model);
/// <summary>
/// Configures the specified fixture's act step to be a PUT request for the specified entity and id with the specified JSON body.
/// </summary>
public static IMvcFunctionalTestFixture WhenUpdating<TId, TModel>(this IMvcFunctionalTestFixture fixture,
string entity, TId id, out TModel model) =>
fixture.WhenCallingRestMethod(HttpMethod.Put, $"{entity}/{Uri.EscapeDataString(id.ToString())}", out model);

/// <summary>
/// Configures the specified fixture's act step to be an HTTP request with the specified method, url and body.
/// </summary>
public static IMvcFunctionalTestFixture WhenCallingRestMethod<TModel>(this IMvcFunctionalTestFixture fixture,
HttpMethod method, string url, out TModel model) =>
fixture.HavingModel(out model)
.WhenCallingRestMethod(method, url, model);

We can use C# 7 inline variable declaration to call these methods in a fluent style.

[Fact]
public Task When_updating_something() =>
new MvcFunctionalTestFixture<Startup>(_output)
.WhenUpdating("something", 1, out UpdateSomethingRequest request)
.ShouldReturnSuccessfulStatus()
.ShouldReturnSomething(request)
.RunAsync();
[Fact]
public Task When_updating_something() =>
new MvcFunctionalTestFixture<Startup>(_output)
.WhenUpdating("something", 1, out UpdateSomethingRequest request)
.ShouldReturnSuccessfulStatus()
.ShouldReturnSomething(request)
.RunAsync();

I think this looks awesome.

I have demonstrated how this library could be extended to support other packages and frameworks by integrating it with MySQL. xunit-fixture-mvc-mysql is also available on GitHub and NuGet. Here's a complete usage example.

[Fact]
public Task When_creating_breakfast_item() =>
new MvcFunctionalTestFixture<Startup>(_output)
.HavingMySqlDatabase<BreakfastContext>()
.WhenCreating("BreakfastItem", out CreateOrUpdateBreakfastItemRequest request)
.ShouldReturnSuccessfulStatus()
.ShouldReturnJson<BreakfastItem>(r => r.Id.Should().Be(1),
r => r.Name.Should().Be(request.Name),
r => r.Rating.Should().Be(request.Rating))
.ShouldExistInDatabase<BreakfastContext, BreakfastItem>(1,
x => x.Id.Should().Be(1),
x => existing.Name.Should().Be(request.Name),
x => x.Rating.Should().Be(request.Rating))
.RunAsync();
[Fact]
public Task When_creating_breakfast_item() =>
new MvcFunctionalTestFixture<Startup>(_output)
.HavingMySqlDatabase<BreakfastContext>()
.WhenCreating("BreakfastItem", out CreateOrUpdateBreakfastItemRequest request)
.ShouldReturnSuccessfulStatus()
.ShouldReturnJson<BreakfastItem>(r => r.Id.Should().Be(1),
r => r.Name.Should().Be(request.Name),
r => r.Rating.Should().Be(request.Rating))
.ShouldExistInDatabase<BreakfastContext, BreakfastItem>(1,
x => x.Id.Should().Be(1),
x => existing.Name.Should().Be(request.Name),
x => x.Rating.Should().Be(request.Rating))
.RunAsync();

The first fluent argument, .HavingMySqlDatabase<BreakfastContext>() will configure the fixture to use the specified DbContext type to create a new, fully migrated database that has been isolated specifically for this test. Then the call to .ShouldExistInDatabase<BreakfastContext, BreakfastItem> will configure the fixture to, after the request has completed, find the BreakfastItem entity in the database with an id of 1 and run the specified assertions on it.

There's some pretty complicated stuff going off in these later examples, yet the tests themselves are concise and readable. All of the boilerplate code necessary for setting up the test server, calling it RESTfully, managing the lifetime of an isolated MySQL database, AutoFixture based model generation and more is hidden away behind reusable bits of code in extension methods. It's a stupidly simple concept but I think it works really well. What do you think?