Developer Notes

Protect yourself from silly mistakes with integration tests

Integration test : Controller Creation in .NET Core

The default dependency injection container in ASP.NET Core is pretty decent but lacks some of the features of the more seasoned containers like Castle Windsor.

One of the things that's sorely lacking, in my opinion, is being able to validate whether the dependency graph is complete. This  used to be one of the first tests I add to a .NET Framework app when using a dependency injection container.

It's quite common for devs to break the dependency graph while evolving the application and it can be a costly mistake. The build will not break but the application will be broken.

A partial fix : validate controller creation

Within ASP.NET Core the entry point for any external request is a controler, so if we ensure the dependency graph is complete for all controllers we'll be able to catch a good portion of mistakes.

One of the nice things that ASP.NET Core brings to the table is the in-memory TestServer, designed specifically to enable integration testing. Testing startup configuration is most definitely an integration test.

Using that test server you can bootstrap your application and run tests against the dependency container.

The Gist below has all the code for a working test.

Test project setup

For this code to execute, you should setup an XUnit test project, either from Visual Studio or from the command line:

dotnet new xunit -o MyProject.Tests

 Then add the following Nuget packages

  • Microsoft.AspNetCore.Mvc.Testing
  • FluentAssertions

Finally, add a reference to your ASP.NET Core web application project. The Startup class is the startup class for your application. If needed you can provide appsettings.json to give your application the required configuration settings.

 

The test code

The test uses the WebApplicationFactory provided by the ASP.NET Core for the TestServer (check the Microsoft docs for more info).

 The constructor will receive the factory and allow you to configure it as needed. The test method itself uses XUnit's Theory to execute once for each controller. The controllers are discovered using reflection. Depending on your setup, you may need to use a different strategy to find the controllers in your application.

Finally, the test itself does some magic to get a hold of the ServiceProvider and tries to create each controller.

The error message will be nice and verbose so you know which controller could not be created.

using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace MyProject.Tests.Configuration
{
public class When_the_application_is_configured : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _factory;
public When_the_application_is_configured(WebApplicationFactory<Startup> factory)
{
_factory = factory.WithWebHostBuilder(config =>
config.ConfigureAppConfiguration( (context, builder) =>
{
// force using local development settings here to ensure valid configuration
builder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
.AddJsonFile($"appsettings.LocalDevelopment.json", optional: true, reloadOnChange: true);
}));
}
[Theory]
[MemberData(nameof(Controllers))]
[Trait("Category", "Integration")]
public void It_should_be_able_to_create_all_controllers(Type controller)
{
// Arrange
using var serviceScope = _factory.Services.CreateScope();
var serviceProvider = serviceScope.ServiceProvider;
// act
var service = serviceProvider.GetRequiredService(controller);
// Assert
service.Should().NotBeNull($"controllers of type {controller.Name} can be created");
}
public static IEnumerable<object[]> Controllers
{
get
{
// Note that this assumes all controllers are in the same assembly as the Startup class
return typeof(Startup)
.Assembly
.GetTypes()
.Where(t => typeof(ControllerBase).IsAssignableFrom(t))
.Select(t => new object[]
{
t
});
}
}
}
}