Testing My Web API using TestServer

Unit Testing my web API using .net 5, TestServer, NUnit, FluentAssertions and Moq.

Contents

Background

I have a while trying to create a post ( or start a blog ) about the topic of testing API's... well, the time is here. In this post I will expose the way I found to test my web API, I also will put a few relevant links that I found helpful during this process.

I know that there are multiple examples out there, including the official ones from Microsoft, and all of them are really helpful; I just hope you find this post relevant for you next or current project.

My apologies in advance for initializing my mocks and test server in the constructor of my unit test class while using NUnit, a better place to do that would be in a [OneTimeSetUp] method.

Well... that's all I have to say before starting the code, let's jump in!

Creating The Web API

For this web API you can find the code in my GitHub

The API consists on a domain driven design architecture. where you will see three different projects : web API project, domain and infrastructure.

I won't explain in details each part; I will limit it to ' it is about tomatoes'. Something important is to use separation of concerns and dependency injection, that will make things easier at the moment of creating out unit tests and mocking up dependencies like repositories and services.

The Test Project

Again, if you prefer to see the complete code you can find it here.

Let's start by creating a new test project, this is a simple Class Library project, we will call it 'Tomappto.Api.Test' and add the following packages. You can use the UI or Package Manager Console for it :

Install-Package Microsoft.NET.Test.Sdk
Install-Package NUnit
Install-Package NUnit3TestAdapter
Install-Package FluentAssertions
Install-Package Moq

Install-Package Microsoft.AspNetCore.Hosting
Install-Package Microsoft.AspNetCore.Mvc.Testing

ApiWebApplicationFactory

Before creating the main test class we are going to create a web factory that will allow us to instantiate our in-memory API. I am adding this class to a folder named 'Extensions'... you don't have to do this.

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using System;

namespace Tomappto.Api.Test.Extensions
{
    internal class ApiWebApplicationFactory<TStartUp>
            : WebApplicationFactory<TStartUp> where TStartUp : class
    {

    }
}

ApiWebApplicationFactory<TStartUp> inherits from WebApplicationFactory<TStartUp> this class will help us to create a TestServer instance using the application defined by TStartUp. It will look for the entry point and use CreateWebHostBuilder(string [] args) to initialize the application.

We are adding a constructor that will receive an Action<IServiceCollersion> and a field where to keep it, later on we will use this action to configure and replace our services.

Action<IServiceCollection> serviceConfiguration { get; }

public ApiWebApplicationFactory(Action<IServiceCollection> serviceConfiguration) : base()
{
   this.serviceConfiguration = serviceConfiguration;
}

The next part is an override for ConfigureWebHost(IWebHostBuilder builder).

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
    if (this.serviceConfiguration != null)
    {
        builder.ConfigureServices(this.serviceConfiguration);
    }
}

builder.ConfigureServices is executed after our Startup.ConfigureServices. We are getting the configuration we established in the constructor and applying it to override our services.

The extension class looks like this:

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using System;

namespace Tomappto.Api.Test.Extensions
{
    internal class ApiWebApplicationFactory<TStartUp>
            : WebApplicationFactory<TStartUp> where TStartUp : class
    {

        Action<IServiceCollection> serviceConfiguration { get; }

        public ApiWebApplicationFactory(Action<IServiceCollection> serviceConfiguration) : base()
        {
            this.serviceConfiguration = serviceConfiguration;
        }

        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            if (this.serviceConfiguration != null)
            {
                builder.ConfigureServices(this.serviceConfiguration);
            }
        }

    }
}

ServiceCollectionExtensions.Replace

When replacing services we need to look if we have the services to be replaced in the queue and, of course, replace them. For that I will create an extension to IServiceCollection where I will pass the mock of the service to be replaced. I created a new static class in our extensions folder and name it ServiceCollectionExtesions and added a static method as shown bellow:

using Microsoft.Extensions.DependencyInjection;

namespace Tomappto.Api.Test.Extensions
{
    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);
                }
            }
        }
    }
}

The Replace method loops through the configured services in our Startup and, if found, it will replace it with the instance provided.

TomapptoApiShould ( the actual unit test class )

Ok, now we can create the fixture class where to test and create our in-memory API. We start with creating a class, I name it TomapptoApiShould.

namespace Tomappto.Api.Test
{
    public class TomapptoApiShould
    {
    }
}

To this class I have added a constructor ( pss... you can use [OneTimeSetUp] if you are using NUnit) where to initialize mocks and our in-memory API. Let's start with our fake data, here we are creating the data that our fake repository will return. Don't forget to add AsQueriable().

public TomapptoApiShould()
{

    var data = new List<Tomato>
    {
        new Tomato(DateTime.Now,"Unit Test Person 1"),
        new Tomato(DateTime.Now,"Unit Test Person 2"),
    }.AsQueryable();

}

Then, following Microsoft recommendations, we will create a mock of our dataset based on the fake data and configure it accordingly :

Mock<DbSet<Tomato>> mDataSet = new Mock<DbSet<Tomato>>();

public TomapptoApiShould()
{

    var data = new List<Tomato>
    {
        new Tomato(DateTime.Now,"Unit Test Person 1"),
        new Tomato(DateTime.Now,"Unit Test Person 2"),
    }.AsQueryable();

    mDataSet.As<IQueryable<Tomato>>().Setup(m => m.Provider).Returns(data.Provider);
    mDataSet.As<IQueryable<Tomato>>().Setup(m => m.Expression).Returns(data.Expression);
    mDataSet.As<IQueryable<Tomato>>().Setup(m => m.ElementType).Returns(data.ElementType);
    mDataSet.As<IQueryable<Tomato>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());


}

Next is to create a mock of ITomatoesRepository to replace it in the app's Startup. For that we will configure that when using ITomatoesRepository.GetTomatoes() we are returning our IQueryable with fake data :

Mock<DbSet<Tomato>> mDataSet = new Mock<DbSet<Tomato>>();
Mock<ITomatoesRepository> mRepository =new Mock<ITomatoesRepository>();

public TomapptoApiShould()
{

    var data = new List<Tomato>
    {
        new Tomato(DateTime.Now,"Unit Test Person 1"),
        new Tomato(DateTime.Now,"Unit Test Person 2"),
    }.AsQueryable();

    mDataSet.As<IQueryable<Tomato>>().Setup(m => m.Provider).Returns(data.Provider);
    mDataSet.As<IQueryable<Tomato>>().Setup(m => m.Expression).Returns(data.Expression);
    mDataSet.As<IQueryable<Tomato>>().Setup(m => m.ElementType).Returns(data.ElementType);
    mDataSet.As<IQueryable<Tomato>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());

    mRepository.Setup(s => s.GetTomatoes()).Returns(mDataSet.Object);


}

Time to use those extensions we were working on before. To the constructor I will add the initialization of a field of type ApiWebApplicationFactory<Tomappto.Api.Startup>. Using the constructor we defined, I will add the proper services configuration and in that service configuration I will use the ServiceCollectionExtensions.Replace to find and replace my repository with my fake one.

Mock<DbSet<Tomato>> mDataSet = new Mock<DbSet<Tomato>>();
Mock<ITomatoesRepository> mRepository =new Mock<ITomatoesRepository>();
private ApiWebApplicationFactory<Tomappto.Api.Startup> testServerFactory;

public TomapptoApiShould()
{

    ... Previous code ...

    testServerFactory = new(services =>
    {
        services.Replace<ITomatoesRepository>(mRepository.Object);
    });

}

Last part of the constructor is to instantiate a field of type HttpClient from our in-memory API. This field will execute the call to our API in our test case.

Mock<DbSet<Tomato>> mDataSet = new Mock<DbSet<Tomato>>();
Mock<ITomatoesRepository> mRepository =new Mock<ITomatoesRepository>();
private ApiWebApplicationFactory<Tomappto.Api.Startup> testServerFactory;
private HttpClient httpClient;

public TomapptoApiShould()
{

    ... Previous code ...

    testServerFactory = new(services =>
    {
        services.Replace<ITomatoesRepository>(mRepository.Object);
    });

    httpClient = testServerFactory.CreateClient();   

}

Now lets move to the test case. We add another method, I called it ReturnTomatoes. In this method I use the httpClient.GetAsync() to get a response from the endpoint '/tomatoes'. That, remember, we replaced the repository that will be injected to the controller behind that endpoint. After calling the endpoint I am validating that I am getting a good respond (200) and checking that it's body contains the fake data previously configured:

[Test]
public async Task ReturnTomatoes() 
{

    var mResponse = await httpClient.GetAsync("/tomatoes");

    mResponse.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);

    var mResponseBody = await mResponse.Content.ReadAsStringAsync();

    mResponseBody.Should().Contain("Unit Test Person 1");
    mResponseBody.Should().Contain("Unit Test Person 2");

}

To help with the assertion of my result I am using FluentAssertions, that makes things easier to check.

And we did it! We are using an in-memory version of our API to execute unit test's without the necessity to have a publication of our application ( or without running our application ). To prevent from using a real database we are mocking up the repository and with this we can test application level functionality.

Wrapping Up

This is a simple test demonstration for the in-memory API. In a more real application I wouldn't be using my constructor to initialize or create the fake data, specially if I am using NUnit. Depending on the test framework of your preference you will find the proper place for that. For NUnit we have the option to use OneTimeSetUp and OneTimeTearDown.

Thank You!

This post was inspired by I problem I found while working with Consumer-Driven Contract Testing. I will be posting more about this topic in my next entry.

Something else: I found a good Stack Overflow with basically the ideas I am posting here.

You can read it here: How to ConfigureServices in Asp.Net core WebAPI from another assembly. Thank you terle!

In that same stack overflow you will see a link to another helpful blog: Setting up integration tests using the ASP.NET Core WebHostBuilder. Thank you Uli Weltersbach!

And thank you for reading my post, I hope this information helps you in one way or another!

And remember, developers work better when coffee is available, Thank you for all that coffee!

yellow-button.png