Skip to content

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.

Published inSoftware Development

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *