Use ASP.NET Core's TestServer in xUnit to test Web API endpoints: TestServer - Part 1

Published: Friday 20 November 2020

Using ASP.NET Core's TestServer allows us to create an in-memory web server. This allows us to write xUnit tests, focusing on endpoints in a ASP.NET Core Web API application.

We go ahead and create our xUnit project. Within that, we go ahead and build up an instance of WebHostBuilder. This includes creating a custom Startup class that allows us to add xUnit test configurations.

This instance of WebHostBuilder is then passed into a new instance of TestServer.

And that's it! We are ready to test our API endpoints.

That's have a look and see how we can create endpoint tests for our ASP.NET Core Web API.

How to Install TestServer in xUnit

Setting up TestServer in our xUnit test project is relatively simple.

But first, we need to create a new xUnit test project if that hasn't been done already.

Assuming we have our ASP.NET Core Web API application open in Visual Studio 2019, right-click on the solution, go to Add and go to "New Project...".

Afterwards, do a search for "xUnit" and click on "xUnit Test Project (.NET Core)".

Create an xUnit project in Visual Studio 2019

Create an xUnit project in Visual Studio 2019

That's the xUnit project set up.

Before we do anything else, we need to make sure that we reference any projects that we are testing in our xUnit project. This will include the ASP.NET Core Web API application that we are testing.

Next thing to do is to install the NuGet package that contains TestServer.

In Visual Studio 2019, go to Tools, NuGet Package Manager and Package Manager Console.

Install the following NuGet package, ensuring that we select the test project as our Default Project.

Install-Package Microsoft.AspNetCore.TestHost

How to Setup TestServer in xUnit

Now that we have the package installed into our xUnit project, we can go ahead and set up TestServer in our xUnit project.

The way I like to do it is to install the TestServer reference in the constructor and make sure that it inherits the IDisposable interface.

By doing that, we can then call the Dispose method, and dispose the instance of the TestServer.

We need to create an instance of WebHostBuilder which we pass in as a parameter when creating a new instance of TestServer.

One of the things we need to do is to pass in a Startup class. Now, we could use the same Startup class that is in our ASP.NET Core Web API application. However, that will cause some issues with the database which we will look at later.

So I prefer to create my own custom Startup class for the xUnit project.

// UnitTest1.cs
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using System;
using Xunit;
 
public class UnitTest1 : IDisposable
{
	protected TestServer _testServer;
	public UnitTest1()
	{
		var webBuilder = new WebHostBuilder();
		webBuilder.UseStartup<Startup>();

		_testServer = new TestServer(webBuilder);
	}

	[Fact]
	public void Test1()
	{

	}

	public void Dispose()
	{
		_testServer.Dispose();
	}
}
// Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using RoundTheCode.CrudApi.Data;
 
namespace RoundTheCode.CrudApi.Tests
{
	public class Startup
	{
		public Startup(IConfiguration configuration)
		{
			Configuration = configuration;
		}

		public IConfiguration Configuration { get; }

		public void ConfigureServices(IServiceCollection services)
		{
			services.AddControllers().AddApplicationPart(Assembly.Load("RoundTheCode.CrudApi.Web")).AddControllersAsServices();

			services.AddDbContext<CrudApiDbContext>();
			services.AddScoped<ILeagueService, LeagueService>();
			services.AddScoped<ITeamService, TeamService>();
		}

		public void Configure(IApplicationBuilder app)
		{
			app.UseRouting();

			app.UseEndpoints(endpoints =>
			{
				endpoints.MapControllers();
			});
		}
	}
}

The Startup class needs to have the Web API methods included for it to run.

So that includes adding the AddControllers method to the IServiceCollection reference, and adding the UseRouting and UseEndpoints methods inside the Configure method.

As the controllers are from another assembly, we have to load in that assembly and then call the AddControllersAsServices method.

Within UseEndpoints method, we need to make sure that we call the MapControllers method.

And, we also need to make sure that we add any services that we are referencing in our IServiceCollection interface.

Running the test in the UnitTest1 class should now pass. We ain't actually running any tests at this stage. We are just checking that our TestServer instance is not throwing an error.

What Happens with Entity Framework in TestServer?

As is shown in our Startup class, we are setting up a reference to an Entity Framework DbContext.

public class Startup
{
	public Startup(IConfiguration configuration)
	{
	...
	}

	public IConfiguration Configuration { get; }

	public void ConfigureServices(IServiceCollection services)
	{
		...
		services.AddDbContext<CrudApiDbContext>();
	}

	...
}

Now we have two options on how we use Entity Framework in xUnit.

Using a Test Database in SQL Server

We can go ahead and create a test database in SQL Server to use in our tests.

This has the benefits where we can test issues to do with SQL Server. For example, we can ensure that indexes, constraints and transactions are working as they should be.

However, it does has it's drawbacks. We have to make sure that the database is cleared down once a test is finished, or before a test is ran.

In-addition, we need to make sure we are pointing to the correct database. It may seem silly, but we could easily be running our tests against a live database, which could be disastrous.

What About Entity Framework's In-Memory Database?

The other alternative is to use Entity Framework's In-Memory database.

The difference with this is that we are testing the functionality of Entity Framework more than our database provider. That is because an in-memory database doesn't connect to a database provider.

With this in mind, we can't test things like indexes, constraints and transactions.

But, we can test what data goes into a particular entity through Entity Framework.

In-order to use Entity Framework's In-Memory database, we need to install the following Nuget package into our xUnit project.

Install-Package Microsoft.EntityFrameworkCore.InMemory

Once that's done, we need to make a tweak when adding the DbContext into our IServiceCollection instance in the Startup class.

We will need to reference the DbContext options, ensuring that we call the UseInMemoryDatabase method, passing in a custom database name as a parameter.

// Startup.cs
...
using Microsoft.EntityFrameworkCore;
using System;
 
namespace RoundTheCode.CrudApi.Tests
{
	public class Startup
	{
		...

		public void ConfigureServices(IServiceCollection services)
		{
			...
			services.AddDbContext<CrudApiDbContext>(options =>
			{
				options.UseInMemoryDatabase("MyDatabase-" + Guid.NewGuid());
			});
		}

		...
	}
}

In addition, inside our DbContext, we need to ensure that we are passing these options in as a constructor. When doing this, we can call the base constructor that will do all the work in configuring the options for our DbContext.

// CrudApiDbContext.cs
namespace RoundTheCode.CrudApi.Data
{
	public class CrudApiDbContext : DbContext
	{
		...

		public CrudApiDbContext(DbContextOptions<CrudApiDbContext> options) : base(options)
		{
		}

		...
	}
}

How to Run an Endpoint in TestServer

With the DbContext setup, we can go ahead and actually test our endpoints.

First, we need to change our test so it runs asynchronously, returning an instance of Task.

Then, we can run our endpoints. We will test the endpoints where we are reading a record.

As we haven't added any data into our in-memory DbContext, we would expect our calls to return a 404 not found response.

...
using System.Net;
using System.Threading.Tasks;
 
namespace RoundTheCode.CrudApi.Tests
{
	public class UnitTest1 : IDisposable
	{
		...

		[Fact]
		public async Task TestReadMethods()
		{
			var response = await _testServer.CreateRequest("/api/league/1").SendAsync("GET");

			Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);

			response = await _testServer.CreateRequest("/api/league/2").SendAsync("GET");
			Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
		}

		...
	}
}

Running those tests should give us a green tick.

You can watch us run these tests, as well as setting up our xUnit project, and configuring our in-memory DbContext in the video below.

What about Handling Post Requests and Limiting Instances of TestServer?

Coming up, we will have a look at some useful TestServer tips, such as firing a post request and limiting the number of instances of TestServer.

In addition, we will also have a look at how we can store data in an Entity Framework in-memory context. And, how we can add an Authorization header to a TestServer request.

That's all available inĀ part 2.