GitHub Repository
A full example of this code can be found at https://github.com/dombarter/Solar.API
Motivation
As you may have seen from my previous posts, I’ve been experimenting with creating a .NET 6 Web API that uses Entity Framework, Identity and JWTs as cookies that can be consumed by a Vue.js SPA. I’ve not delved into the world of integration tests too much but thought it would be a really interesting concept to create an entire test database and test our full controller lifecycle, as this will catch errors at any level.
Create Your Test Project
The first step is to make a xUnit
test project to complement your API
project.
Our API
project is called Solar.API
and we created a new xUnit
project called Solar.API.Test
.
NuGet Packages
You need to install the following NuGet packages:
Microsoft.AspNetCore.Mvc.Testing
- for mocking the controllersMicrosoft.EntityFrameworkCore.InMemory
- for creating an empty test database
API Startup Class
Our integration tests need to access our startup class, Program.cs
so they can create a mock of the controllers. We need to make some slight adjustments to the access levels because by default it is only internally accessible.
Go to Program.cs
and add this line at the bottom of the file:
app.Run();
public partial class Program {} // <-- add this line!
Integration Tests Base Class
Now we need to make a base class that all our integration tests will inherit. In this class we will provide a few important things:
GenerateClient
- for creating a mock of the controllers, and swapping out our SQL Server for an empty in-memory equivalent- Authentication helper methods
GenerateClient
The GenerateClient
performs the following:
- Creates a new
WebApplicationFactory
based on ourAPI
startup class - It then amends the startup class, by changing some of the services
- It firstly removes the existing
DbContext
service - It then replaces it with a new
DbContext
class that uses an in-memory database with a unique name (this ensures each integration test has an empty database to work with) - It finally defines a custom
https
endpoint for the mocked controller, meaning ourSecure
JWT cookies can be transmitted
public class IntegrationTestsBase
{
protected HttpClient GenerateClient()
{
// Build up the API, using an in memory database
var application = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Remove the db context
services.RemoveAll(typeof(DbContextOptions<SolarDbContext>));
// Replace the connection with an in memory equivalent
var dbName = Guid.NewGuid().ToString();
services.AddDbContext<SolarDbContext>(options =>
{
options.UseInMemoryDatabase(dbName);
});
});
});
// Generate the client
var client = application.CreateClient(new WebApplicationFactoryClientOptions { BaseAddress = new Uri("https://localhost") });
return client;
}
}
Write Some Tests!
You can now write some tests. Here are some examples of what you can do:
Making Sure Endpoints Are Protected
in this example we make GET
requests to multiple endpoints we know should be protected under normal circumstances. We make sure that a 401 code is returned.
public class MoonControllerTests : IntegrationTestsBase
{
[Theory]
[InlineData("/moons/one")]
[InlineData("/moons/two")]
public async Task Get_WhenUnauthenticated_ReturnsUnauthorized(string url)
{
// Arrange
var client = GenerateClient();
// Act
var response = await client.GetAsync(url);
// Assert
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
}
Registering & Logging In
In this example we register a new user, then login as them and make sure that both requests return an OK
response indicating the request was a success.
public class AccountControllerTests : IntegrationTestsBase
{
[Fact]
public async Task Post_Login_ReturnsOK()
{
// Arrange
var client = GenerateClient();
// Act
var register = await Register(client, new RegisterDto
{
Email = "[email protected]",
Password = "password"
});
var login = await Login(client, new LoginDto
{
Email = "[email protected]",
Password = "password"
});
// Assert
Assert.Equal(HttpStatusCode.OK, register.StatusCode);
Assert.Equal(HttpStatusCode.OK, login.StatusCode);
}
}
Where Login
and Register
are some helper methods I was talking about earlier:
protected async Task<HttpResponseMessage> Register(HttpClient client, RegisterDto registerDto)
{
// Act
var response = await client.PostAsJsonAsync("/user/register", registerDto);
return response;
}
protected async Task<HttpResponseMessage> Login(HttpClient client, LoginDto loginDto)
{
// Act
var response = await client.PostAsJsonAsync("/user/login", loginDto);
return response;
}
Requesting An Endpoint When Authenticated
In this example, we authenticate the request by registering and logging in using a helper method. This stores a JWT cookie. We then make a request to an endpoint that would usually be protected and make sure that the response is OK
.
public class MoonControllerTests : IntegrationTestsBase
{
[Fact]
public async Task Get_OneMoon_AsUser()
{
// Arrange
var client = GenerateClient();
await AuthenticateAsUser(client);
// Act
var response = await client.GetAsync("/moons/one");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}
References
Based on the following websites: