Web API testing in ASP.NET Core

ASP.NET Core provides great support for integration testing of Web APIs. You can host the server in the test process and still make requests over HTTP:

this.server = new TestServer(new WebHostBuilder()
    .UseStartup<Startup>();
this.Client = this.server.CreateClient();

var value = await this.Client.GetStringAsync("config/Key1");

However, if your app reads its configuration from the appsetting.json file, you'll quickly find that the test server cannot find your regular configuration file.

Let's say I have the following controller to retrieve values from my configuration file:

[Route("[controller]")]
[ApiController]
public class ConfigController : ControllerBase
{
    private readonly IConfiguration configuration;

    public ConfigController(IConfiguration configuration)
    {
        this.configuration = configuration;
    }

    [HttpGet]
    [Route("{key}")]
    public string Get(string key)
    {
        return configuration[$"Settings:{key}"];
    }
}

For this to work, I'd need to have a Settings section in the appsetting.json file of my Web API project:

{
  "Settings": {
    "Key1": "Value 1"
  }
}

With the above configuration, the config/Key1 endpoint would return Value 1. However, the following test would fail:

[Test]
public async Task GetConfig()
{
    this.server = new TestServer(new WebHostBuilder()
        .UseStartup<Startup>();
    this.Client = this.server.CreateClient();

    var value = await this.Client.GetStringAsync("config/Key1");

    Assert.That(value, Is.EqualTo("Value 1"));
}

When running inside the test project, the test server cannot find the configuration file of the Web API project. There are several ways to fix this:

  • You could create a different configuration file in the test project.
  • You could modify your build to copy the configuration file from the Web API project to the build output folder of the test project.
  • You could configure the test server to find the configuration file in the Web API project.

I decided to go with the last option. It seemed the easiest to implement and has worked well enough for me so far.

I added custom configuration to my test server setup:

var configuration = new ConfigurationBuilder()
    .AddJsonFile(
        Path.Combine(settingsFilePath, "appsettings.json"),
        optional: true,
        reloadOnChange: true)
    .AddJsonFile(
        Path.Combine(settingsFilePath, "appsettings.Development.json"),
        optional: true,
        reloadOnChange: true)
    .Build();

this.server = new TestServer(new WebHostBuilder()
    .UseStartup<Startup>()
    .UseConfiguration(configuration));

I hid the hardcoded path to the configuration files in the private settingsFilePath field:

private readonly string settingsFilePath = Path.GetFullPath(
    Path.Combine("..", "..", "..", "..", "WebApiTesting"));

With this updated configuration, the above test passed.

To avoid repeating the setup code in every test, I moved it to a test base class:

public abstract class WebApiTestBase
{
    private readonly string settingsFilePath = Path.GetFullPath(
        Path.Combine("..", "..", "..", "..", "WebApiTesting"));

    private TestServer server;

    protected HttpClient Client { get; private set; }

    protected abstract string ApiRoute { get; }

    [SetUp]
    public void Setup()
    {
        var configuration = new ConfigurationBuilder()
            .AddJsonFile(
                Path.Combine(settingsFilePath, "appsettings.json"),
                optional: true,
                reloadOnChange: true)
            .AddJsonFile(
                Path.Combine(settingsFilePath, "appsettings.Development.json"),
                optional: true,
                reloadOnChange: true)
            .Build();

        this.server = new TestServer(new WebHostBuilder()
            .UseStartup<Startup>()
            .UseConfiguration(configuration));
        this.Client = this.server.CreateClient();

        this.Client.BaseAddress = new Uri(
            this.Client.BaseAddress.ToString() + this.ApiRoute);
    }
}

The abstract ApiRoute property is used to avoid repeating the controller base path in each test, since I can now easily define it at the test class level:

public class ConfigTests : WebApiTestBase
{
    protected override string ApiRoute => "config/";

    [Test]
    public async Task GetConfig()
    {
        var value = await this.Client.GetStringAsync("Key1");

        Assert.That(value, Is.EqualTo("Value 1"));
    }
}

Individual tests need only contain the action method part of the path.

You can find working example code for this approach in my GitHub repository.

ASP.NET Core includes a test server that can be used for in-process Web API integration tests. By default, when the application needs a configuration file, it'll try to read it from the test project. In this post, I've described my approach to reading the application configuration file from the test project.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License