Introduction
WebApplicationFactory
(from Microsoft.AspNetCore.Mvc.Testing) is a great tool for testing ASP.NET (Core) WebAPIs (I have a whole series on integration tests with ASP.NET (Core) and SQL Server, with one part specifically dedicated to WebApplicationFactory).
It’s easy create an HTTP client from the factory. The basic setup is just two lines:
var factory = new WebApplicationFactory<Startup>();
HttpClient client = factory.CreateClient();
If we want some customization, we can create an inherited class TestApplicationFactory
, override ConfigureWebHost
method and do the setup there:
public class TestApplicationFactory : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration((context, configurationBuilder) =>
{
// can add some custom configuration...
});
builder.ConfigureServices((context, services) =>
{
// can do something based on the context or the configuration...
});
}
}
For example, you can configure the test application to use a different connection string for the database.
Problem
What if we need to create multiple kinds of clients (and therefore factories)?
For example, we want all our tests for working with the data to bypass the authorization, with the exception of the actual tests for the authorization.
We could create a whole new factory for the client which would be used for authorization tests. However, the authorization service is the only difference from the “original” (default) client. We still want to use default configuration of the original factory that bypasses the authorization, except we don’t want to bypass it.
Therefore, we need to be able to inject some configuration to the factory from the outside.
Solution
We add a constructor to TestApplicationFactory
that takes Action<IServiceCollection>
as a parameter. We will then use this parameter when calling builder.ConfigureServices()
:
public class TestApplicationFactory : WebApplicationFactory<Startup>
{
// delegate that can be injected from outside
private readonly Action<IServiceCollection> configureServices;
// parameterless constructor in case we want to use the default setting
public TestApplicationFactory() { }
public TestApplicationFactory(Action<IServiceCollection> configureServices)
{
this.configureServices = configureServices;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration((context, configurationBuilder) => { ... });
builder.ConfigureServices((context, services) =>
{
// e.g. set the tests to bypass the authorization
});
// don't forget to check for null
if (configureServices is null) return;
// use configureServices delegate
builder.ConfigureServices(configureServices);
}
}
Now the only question is, how to implement this configureServices
delegate?
Working with the services
In all the examples that follow, services
varaible is of type that implements IServiceCollection
.
If we have a interface that has only one implementation registered, we can just call Replace
method that removes the existing implementation and registers another one.
services.Replace(ServiceDescriptor.Scoped<IUserService, MockUserService>);
We don’t even have to create a new class for the custom implementation, we can use a mocking library like Moq:
// create a mock
var userServiceMock = new Mock<IUserService>();
// define behaviour for some (or all) methods
userServiceMock.Setup(usm => usm.GetCurrentUserName()).Returns("test user");
// do the replacement
services.Replace(ServiceDescriptor.Scoped(typeof(IUserService), _ => userServiceMock.Object));
If there is more than one implementation registered (e.g. multiple authorization handlers), we can’t just call Replace
, because it only replaces the first implementation that it finds. Instead, we first have to call RemoveAll
and then Add
the desired implementation (e.g. AllowAnonymousAuthorizationHandler):
services.RemoveAll<IAuthorizationHandler>();
services.AddSingleton<IAuthorizationHandler, AllowAnonymousAuthorizationHandler>();
We can of course add some implementations (e.g. when re-adding our custom authorization):
services.AddSingleton<IAuthorizationHandler, MyAuthorizationHandler1>();
services.AddSingleton<IAuthorizationHandler, MyAuthorizationHandler2>();
Putting it all together
So this is how ConfigureWebHost
method in our TestApplicationFactory
can look like in the end:
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration((context, configurationBuilder) => { ... });
builder.ConfigureServices((context, services) =>
{
var userServiceMock = new Mock<IUserService>();
userServiceMock.Setup(usm => usm.GetCurrentUserName()).Returns("test user");
services.Replace(ServiceDescriptor.Scoped(typeof(IUserService), _ => userServiceMock.Object));
services.RemoveAll<IAuthorizationHandler>();
services.AddSingleton<IAuthorizationHandler, AllowAnonymousAuthorizationHandler>();
});
if (configureServices is null) return;
builder.ConfigureServices(configureServices);
}
And the configureServices
delegate can look like:
var configureServices = services =>
{
services.AddSingleton<IAuthorizationHandler, MyAuthorizationHandler1>();
services.AddSingleton<IAuthorizationHandler, MyAuthorizationHandler2>();
}
We can then decide whether we want to use the factory with the default configuration:
var defaultFactory = new TestApplicationFactory<Startup>();
Or pass in our custom one:
var parametrisedFactory = new TestApplicationFactory<Startup>(configureServices);
Last modified on 2022-01-07