ASP.NET CORE Integration tests: API
All source code can be found at my GitHub, clone repository, and run all tests.
In this blog post, I will cover how to use WebApplicationFactory
and how to build IWebHost
and IHost
manually for testing. Then we will see how to replace services for both hosting types without custom mock services as well as Moq
.
For testing, we will use the default WeatherForecastController
, all logic will be moved to service in order to demonstrate how to replace that service for testing.
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly IWeatherService _weatherService;
public WeatherForecastController(IWeatherService weatherService)
{
_weatherService = weatherService;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
return _weatherService.Get();
}
}
public class WeatherService : IWeatherService
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
Testing with WebApplicationFactory
The easiest and fastest way to get started writing tests. Host
building logic will be taken care of for you. However mocking services will be much more difficult compared to building Host
by yourself.
In both cases IHost
and IWebHost
test class will be identical.
public class WeatherForecastControllerTests : IClassFixture<TestHostProjectFactory>
{
private HttpClient _client;
public WeatherForecastControllerTests(TestHostProjectFactory factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetWeatherForecast()
{
var result = await _client.GetStringAsync("WeatherForecast");
var expected = WeatherServiceMock.FakeData;
var actual = JsonConvert.DeserializeObject<IEnumerable<WeatherForecast>>(result);
var comparer = new CompareLogic();
var compareResult = comparer.Compare(expected, actual);
Assert.True(compareResult.AreEqual, compareResult.DifferencesString);
}
}
WebHost
public class TestWebHostProjectFactory : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder
.UseEnvironment("Test")
.ConfigureTestServices(services =>
{
services.Replace(ServiceDescriptor.Transient<IWeatherService, WeatherServiceMock>());
});
}
}
Host
public class TestHostProjectFactory : WebApplicationFactory<Startup>
{
protected override IHost CreateHost(IHostBuilder builder)
{
builder
.UseEnvironment("Test")
.ConfigureServices(services =>
{
services.Replace(ServiceDescriptor.Transient<IWeatherService, WeatherServiceMock>());
});
return base.CreateHost(builder);
}
}
When comparing both WebApplicationFactories we can see that there is only one difference. WebHost
ConfigureTestServices
and in Host
we have ConfigureServices
.
This method has one drawback, that you have to know and mock all services. For example, if you want to mock ServiceA
for one test and ServiceB
for another test, then you would have some issues.
Create host directly in test
Previously we looked at how to replace service in WebApplicationFactory
but it really limits tests, since tests need different services to be mocked. By creating host directly in tests we will have fine-grained control on which services are mocked and even take control of make things easier.
WebHost
[Fact]
public async Task GetWeatherForecastMoq()
{
var mock = new Mock<IWeatherService>();
using var host = new HostBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder
.UseTestServer()
.UseStartup<Startup>()
//ConfigureTestServices must be bellow UseStartup
.ConfigureTestServices(services =>
{
services.Replace(ServiceDescriptor.Transient(_ => mock.Object));
});
})
.Build();
await host.StartAsync();
mock.Setup(_ => _.Get()).Returns(WeatherServiceMock.FakeData);
var _client = host.GetTestServer().CreateClient();
var result = await _client.GetStringAsync("WeatherForecast");
var expected = WeatherServiceMock.FakeData;
var actual = JsonConvert.DeserializeObject<IEnumerable<WeatherForecast>>(result);
var comparer = new CompareLogic();
var compareResult = comparer.Compare(expected, actual);
Assert.True(compareResult.AreEqual, compareResult.DifferencesString);
}
Host
[Fact]
public async Task GetWeatherForecastMoq()
{
var mock = new Mock<IWeatherService>();
using var host = new HostBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder
.UseTestServer()
.UseStartup<Startup>();
})
.ConfigureServices(services =>
{
services.Replace(ServiceDescriptor.Transient(_ => mock.Object));
})
.Build();
await host.StartAsync();
mock.Setup(_ => _.Get()).Returns(WeatherServiceMock.FakeData);
var _client = host.GetTestServer().CreateClient();
var result = await _client.GetStringAsync("WeatherForecast");
var expected = WeatherServiceMock.FakeData;
var actual = JsonConvert.DeserializeObject<IEnumerable<WeatherForecast>>(result);
var comparer = new CompareLogic();
var compareResult = comparer.Compare(expected, actual);
Assert.True(compareResult.AreEqual, compareResult.DifferencesString);
}
As in a previous example there is only one main difference, WebHost
has special method to configure services ConfigureTestServices
while Host
has to call default ConfigureServices
method. Also it is really important where you call service configuratiom method. Service configuration method must be after UseStartup
.
Code cleanup
As in all final steps, lets do some code cleanup and make tests a bit easier to read and code more reusable.
public class HttpClientFactory
{
public async Task<HttpClient> GetAsync(Action<IServiceCollection> configureServices)
{
var host = new HostBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder
.UseTestServer()
.UseStartup<Startup>();
})
.ConfigureServices(configureServices)
.Build();
await host.StartAsync();
return host.GetTestServer().CreateClient();
}
}
[Fact]
public async Task GetWeatherForecastMoqCustomBuilder()
{
var mock = new Mock<IWeatherService>();
var _client = await new HttpClientFactory()
.GetAsync(services =>
{
services.Replace(ServiceDescriptor.Transient(_ => mock.Object));
});
mock.Setup(_ => _.Get()).Returns(WeatherServiceMock.FakeData);
var result = await _client.GetStringAsync("WeatherForecast");
var expected = WeatherServiceMock.FakeData;
var actual = JsonConvert.DeserializeObject<IEnumerable<WeatherForecast>>(result);
var comparer = new CompareLogic();
var compareResult = comparer.Compare(expected, actual);
Assert.True(compareResult.AreEqual, compareResult.DifferencesString);
}
Building Host vs using default Host builder
In these examples I created Host
from scratch, however, most projects use default host builder.
new HostBuilder()
Switching to default builder saves some time, since it configures quite a few things for you, including settings.
Host.CreateDefaultBuilder()
Summary
In this post, we saw how easy it is to start writing integration tests for API projects. We covered two different hosting methods IWebHost
and IHost
and identified the main differences on how to mock services. While WebApplicationFactory
is easier to get started, manually creating host in tests gives much better control of how services are mocked.