Reason
It’s a real pleasure to write tests for ASP.NET Core, be it unit or integration.
When it comes to writing integration tests, the two main concerns are wiring up an application which behaves as close to the real deal as possible, and replacing any required or desired parts with mocks.
In the following I’ve summarized how we simplify writing integration tests for our service and data layers by using the standard WebHostBuilder to do the grunt work of wiring up the dependencies for our SUT.
Code
Our HttpContext
-agnostic integration tests are all set up following the same pattern:
- Wire up a
WebHostBuilder
which in turn builds a IWebHost.
1.1 Default services are automatically added using theConfigureServices
method of the application’s startup class.
1.2 Mocks/stubs/spies are injected as needed. - Use the IServiceProvider exposed via the
IWebHost.Services
property to get the SUT. - Run tests against the SUT.
The following code uses NUnit for assertions and NSubstitute for mocking.
WebHostBuilderFactory
The WebHostBuilderFactory
facilitates wiring up a WebHostBuilder
with any modifications required by our tests, e.g. replacing dependencies with mocks, stubs or spies:
using System; using System.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; internal static class WebHostBuilderFactory { public static IWebHostBuilder Create<TStartup>( params Action<IServiceCollection>[] testSpecificServiceConfigurations) where TStartup : class { var contentRootPath = Path.Combine(Directory.GetCurrentDirectory(), @"..\..\src\App"); return new WebHostBuilder().UseContentRoot(contentRootPath) .UseEnvironment(EnvironmentName.Development) .UseStartup<TStartup>() .ConfigureServices(services => { // Modify services as required. foreach (var serviceConfiguration in testSpecificServiceConfigurations) { serviceConfiguration(services); } }); } }
ServiceCollectionExtensions
The ServiceCollectionExtensions
facilitate mock injection. Having a toolbox of extension methods to replace often used services helps keep the test suite readable and to the point:
using System; using Microsoft.AspNet.Authentication; using Microsoft.Extensions.DependencyInjection; using NSubstitute; internal static class ServiceCollectionExtensions { public static void Replace<TRegisteredType>(this IServiceCollection services, TRegisteredType replacement) { for (var i = 0; i < services.Count; i++) { if (services[i].ServiceType == typeof (TRegisteredType)) { services[i] = new ServiceDescriptor(typeof (TRegisteredType), replacement); } } } public static void SetSystemClock(this IServiceCollection services, DateTime date) { var clock = Substitute.For<ISystemClock>(); clock.UtcNow.Returns(new DateTimeOffset(date, TimeZoneInfo.Local.GetUtcOffset(date))); services.Replace(clock); } }
Example
The following is a simple test which uses the code shown above to create a ServiceProvider
wired up with all the default services of the application and mocked versions of IMyDependency
and ISystemClock
:
[Test] public void MyServiceDoesAwesomeStuffTwoYearsAgo() { // Arrange var myDependency = Substitute.For<IMyDependency>(); var webHostBuilder = WebHostBuilderFactory.Create<MyStartup>( services => services.Replace<IMyDependency>(myDependency), services => services.SetSystemClock(DateTime.Now.AddYears(-2))); var serviceProvider = webHostBuilder.Build().Services; var myService = serviceProvider.GetRequiredService<IMyService>(); var expected = "awesome stuff"; // Act var actual = myService.DoStuff(); // Assert Assert.That(actual, Is.EqualTo(expected)); }
This was a great guide, but I image that asp.net core updates since then have made the ‘ConfigureServices’ method called before the Startup’s ConfigureServices method.
Instead, use ‘ConfigureTestServices’ (same syntax), which runs after Startup ConfigureServices and allows you to replace services as you would expect