Saturday, September 30, 2017

How to integration test ASP.NET Core services, including Startup and dependency injection

After 3 years, I feel I have something valuable to share.

I have recently started to work with ASP.NET Core and even though it's a thousand times easier to test than ASP.NET MVC, thanks to its built-in Dependency Injection (IServiceCollection, IServiceProvider) and the very great TestServer, I could not find a well rounded pattern to easily test all aspects of an application, mocking at different levels and using the powerful fact that everything comes from the IServiceProvider by design.

This is very much work in progress so please, if you happen to come across this page, get in touch with any alternatives you have found or any improvements you think can be applied.

Maintaining my style, let's get on with some code. It's worth more than a thousand words.

This is what I consider the ideal integration/acceptance/end-to-end test:

public async Task WhenXThenY(
IRepository<SomeEntity> repository, // For this to work, a TestServer must have been started and
// this IRepository must be the same instance that the Controller is going to use when processing the request
WebApiClient client, // This must be wrapping an HttpClient connected to the TestServer
ISomeExternalServiceGateway gateway // Must also be the same instance to be used by the Controller)
{
var entityInRequiredState = CreateEntityInStateX();
repository.GetById(entityInRequiredState.Id).Return(entityInRequiredState);
await client.Post($"controller/action/{entityInRequiredState.Id}");
entityInRequiredState.ShouldBeInStateY();
}
https://gist.github.com/rodolfograve/16e603a86565536063932e03fa4afc10#file-aspnetcoretest-cs

There are a few things to notice here:

  1. Somehow, a TestServer must be started using as much of the production configuration as possible. Obviously, we still need to be able to mock what we want.
  2. The WebApliClient must send its requests to the TestServer.
  3. The IRepository must be the same that is going to be used by the Controller when processing the request, otherwise our setup is useless.
  4. We need to use a framework that lets you decorate your tests to customize the way the parameters are injected. I'm currently using AutoFixture.

The key to make this work is to use the IServiceProvider used by the server as the source for all the instances you want to configure to setup your test. Turns out this is not trivial to achieve without doing some research and understanding some ASP.NET Core secrets, which is why I think this is a valuable thing to share.

The typical code to start an ASP.NET Core web server is:

https://gist.github.com/rodolfograve/16e603a86565536063932e03fa4afc10#file-defaultaspnetcoreprogram-cs

The trick to get access to the IServiceProvider created by the WebHostBuilder, and the reason I'm sharing all this, is to provide an instance of IStartup instead of the type:

https://gist.github.com/rodolfograve/16e603a86565536063932e03fa4afc10#file-modifiedaspnetcoreprogram-cs-L13

With the above in place, we can now use AutoFixture to create an instance of TestServer, keep a reference to the IServiceProvider and use it to obtain instances to be injected into the test:


  1. The attribute, to instruct AutoFixture how to resolve all parameters of the test method.
  2. The AutoFixture ICustomization that creates a fixture for the TestServer and configures a SpecimenBuilder to delegate all requests to the IServiceProvider.
Apologies for not embedding the code here but Blogger refuses to accept any of the solutions I have found to embed code. I do dislike Blogger a lot but I haven't had the time and energy required to find something else.

I will probably turn all the above into a little NuGet package. In the meantime, the gist contains all you need to get it working:



No comments: