Four useful tips when using ASP.NET Core's TestServer in xUnit: TestServer - Part 2

Published: Wednesday 25 November 2020

We are going to have a look at four useful tips when using ASP.NET Core's TestServer in a xUnit project.

First, we will look at a solution where we are able to access the TestServer's DbContext instance from within a test.

And, we will look at how we can handle a POST request in TestServer. It's not quite as simple as handling a GET request.

Afterwards, we will demonstrate how to add an Authorization header in TestServer. Handy if API endpoints require authorisation.

And finally, we will have a look at how we can avoid creating a separate instance of TestServer when running each test.

InĀ part 1, we had a look at how we can install TestServer onto a xUnit project. We wrote tests for our xUnit project, focusing on testing our ASP.NET Core Web API endpoints to see if they work in the way they should.

We are now going to progress further with some useful tips to get the most out of using TestServer.

Tip #1: TestServer's DbContext Instance

By default, when adding a DbContext into dependency injection, the default service lifetime is scoped.

This means that every time we throw a request to TestServer, it creates a new scope. A new scope means a new instance of the DbContext.

Now, if we are testing using a real SQL Server database, this isn't a problem. Because the data is stored in the SQL Server database which can be retrieved when testing our endpoints.

However, if we are using Entity Framework's InMemory DbContext, then it is a problem. That is because we are not storing the data anywhere.

So every time we create a request in TestServer, we are effectively creating a new instance of DbContext.

However, we can get around this by changing the service lifetime of the DbContext. We can change it from a scoped lifetime to a singleton lifetime.

And it's relatively straight forward to do. When we add the DbContext to the IServiceCollection instance, we can pass in a parameter which tells us the service lifetime of the DbContext.

// Startup.cs
public class Startup
{
	public Startup(IConfiguration configuration)
	{
		Configuration = configuration;
	}

	...

	public void ConfigureServices(IServiceCollection services)
	{
		...

		services.AddDbContext<CrudApiDbContext>(options =>
		{
			options.UseInMemoryDatabase("MyDatabase-" + Guid.NewGuid());
		}, ServiceLifetime.Singleton);

		...
	}

	...
}

Tip #2: Post Request on TestServer

Our second tip will look at how we can call a post request on TestServer.

If we are testing an ASP.NET Core Web API endpoint, this will be useful for when we are testing a Create endpoint as these are typically called using a post request.

As we are sending the content as a JSON format, we need to specify the type as "application/json".

In this test below, we are creating a record in our league table. This is done by calling our Create endpoint, sending a POST request.

Afterwards, we are going to call the read endpoint to ensure that the record has been successfully created.

// UnitTest1.cs
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using System;
using System.Net;
using System.Threading.Tasks;
using Xunit;
using System.Net.Http;
using System.Collections.Generic;
using System.Net.Http.Headers;
using Newtonsoft.Json;
using System.Text;
 
public class UnitTest1 : IDisposable
{
	protected TestServer _testServer;
	public UnitTest1()
	{
		var webBuilder = new WebHostBuilder();
		webBuilder.UseStartup<Startup>();

		_testServer = new TestServer(webBuilder);
	}

	[Fact]
	public async Task TestCreateMethod()
	{
		var request = new HttpRequestMessage(HttpMethod.Post, "/api/league");

		request.Content = new StringContent(JsonConvert.SerializeObject(new Dictionary<string, string>
		{
		   {"Name", "MyLeague"}
		}), Encoding.Default, "application/json");

		var client = _testServer.CreateClient();
		client.DefaultRequestHeaders.Clear();
		client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

		var response = await client.SendAsync(request);

		response = await _testServer.CreateRequest("/api/league/1").SendAsync("GET");

		Assert.Equal(HttpStatusCode.OK, response.StatusCode);
	}

	...

	public void Dispose()
	{
		_testServer.Dispose();
	}
}

Tip #3: Add Authorization Header into a TestServer Request

Adding an authorization header into a TestServer request is relatively straight forward.

If we are calling a GET request, we can just call the AddHeader method once we have created the request.

response = await _testServer.CreateRequest("/api/league/2").AddHeader("Authorization", "bearer {token}").SendAsync("GET");

And it's a similar story if we are doing a POST request. Once we have created the request, we can call an instance of Headers and use the Add method from it.

// UnitTest1.cs
public class UnitTest1 : IDisposable
{
	...

	[Fact]
	public async Task TestCreateMethod()
	{
		var request = new HttpRequestMessage(HttpMethod.Post, "/api/league");
		request.Headers.Add("Authorization", "bearer {token}");

		...
	}

	...
}

Tip #4: Use The Same Instance of TestServer in All Tests

By default, every time we run a test, it creates a new instance of that test before running it.

So, even if our xUnit test class has multiple test methods, every time it runs that test, it runs the test class's constructor before executing the test method.

In our tests so far, we have created an instance of TestServer in the constructor.

However, if we have multiple tests running at the same time, we could potentially have a number of instances of TestServer running in the background.

But, we can get around that by creating the instance in a separate class and inherit the class as part of the test, wrapping it around as a generic type in the IClassFixture interface.

// Initialisation.cs
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
 
namespace RoundTheCode.CrudApi.Tests
{
	public class Initialisation
	{
		public TestServer TestServer { get; }

		public Initialisation()
		{
			var webBuilder = new WebHostBuilder()
			.UseStartup<Startup>();

			TestServer = new TestServer(webBuilder);
		}
	}
}
// UnitTest1.cs
namespace RoundTheCode.CrudApi.Tests
{
	public class UnitTest1 : IDisposable, IClassFixture<Initialisation>
	{
		protected TestServer _testServer;
		public UnitTest1(Initialisation initialisation)
		{
			_testServer = initialisation.TestServer;
		}
		 
		...        
	}
}

This means that every time a test is called in UnitTest1, it calls the instance of Initalisation.

With our instance of TestServer created when we initalise an instance of Initalisation, we can call the same instance of TestServer for each of our tests.

Bonus Tip: Add appsettings.json to xUnit Project

Watch our ASP.NET Core coding tutorial where we give a bonus tip of how we can add a custom AppSettings.json file to our xUnit project.

In-addition, we will show the other four tips mentioned in this article, such as creating a POST request in TestServer, and how to add an Authorization header to your TestServer request.