Follow me on Twitter at @tim_deschryver | Subscribe to the Newsletter | Originally published on timdeschryver.dev.
Read the TLDR version on timdeschryver.dev
I really like to write functional tests for my code.
The biggest reason is that each test represents a client that interacts with the application.
Just like a real client, the test doesn't care about the internal details of the application. The test only cares about the result.
Because of it, existing test cases don't need to be touched while we're refactoring the code base, for example, when we're migrating to the new ASP.NET 6 Minimal Web API structure.
To be fair... in this case, some small changes are required.
Luckily, these changes are minor and are only affecting the setup of the tests suites.
The tests themselves remain untouched.
This gives us the confidence to ship our application when the migration is complete.
In How to test your C# Web API we learned how to write functional tests.
To freshen up our memory, here's the base setup that is used in that article.
public class ApiWebApplicationFactory : WebApplicationFactory<Startup>
{
public IConfiguration Configuration { get; private set; }
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration(config => {});
builder.ConfigureTestServices(services => {});
}
}
If you look closely, you'll notice that the WebApplicationFactory
is created by using the Startup
type from the Web API project.
This causes to be the problem because Startup
doesn't exist anymore in a Minimal Web API.
More info about the new structure in Maybe it's time to rethink our project structure with .NET 6.
Fortunately, this doesn't mean that we have to rethink and rewrite our tests (setup).
Reference a Minimal Web API
There is one special file in a Minimal Web API, which is the one that uses top-level statements (there can only be one file like this). This file can be referenced as Program
, and unusually the file is also named Program.cs
(the name of the file doesn't affect the Program
type name).
This Program
type has the internal
access modifier and thus can't be referenced from outside the project by default.
To make it accessible in the test project, we need to give the test project access to the internals of the API Project.
This is done by adding an InternalsVisibleTo
attribute to the API csproj
.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="ApiProject.Tests" />
</ItemGroup>
</Project>
Afterward, the Program
type (instead of Startup
) can be used in the test project to create the application.
public class ApiWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration(config => {});
builder.ConfigureTestServices(services => {});
}
}
But we're not there yet.
When we try to create the application by using Project
, we get the compile error CS0060.
public class ApiWebApplicationFactory : WebApplicationFactory<Program>
~~~~~~~~~~~~~~~~~~~~~~~~
base class 'WebApplicationFactory<Program>'
is less accessible than class 'ApiWebApplicationFactory'
{
}
This can quickly be fixed by also modifying the access modifier of our class to internal
.
internal class ApiWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration(config => {});
builder.ConfigureTestServices(services => {});
}
}
If you're manually spawning new applications then you're job is done here.
You can now spawn a new instance of the API application and test it, just like before.
public class WeatherForecastControllerTests {
[Fact]
public async Task GET_retrieves_weather_forecast()
{
var api = new ApiWebApplicationFactory();
var forecast = await api.CreateClient().GetAndDeserialize<WeatherForecast[]>("/weatherforecast");
forecast.Should().HaveCount(7);
}
}
Though, another problem appears with this code if you're using XUnit fixtures to create the API application.
It might be worthwhile to continue to read, even if you're not using XUnit, because the following solution might have your preference.
Working with XUnit Fixtures
Because we've changed the test's class to internal, we get the next compiler error, xUnit1000 Test classes must be public
.
internal class WeatherForecastControllerTests : IClassFixture<ApiWebApplicationFactory>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
xUnit1000 Test classes must be public
{
private readonly ApiWebApplicationFactory _fixture;
internal WeatherForecastControllerTests(ApiWebApplicationFactory fixture)
{
_fixture = fixture;
}
[Fact]
public async Task GET_retrieves_weather_forecast()
{
}
}
The workaround for this is to reference a public type from the API project.
This can be any type you'd want, but it seems like creating your own Program
type is the desired way to do this.
var builder = WebApplication.CreateBuilder(args);
builder.Services.RegisterModules();
var app = builder.Build();
app.MapEndpoints();
app.Run();
public partial class Program { }
After this, we can revert the changes to make the internals of the API projects visible.
Next, the Program
type can now be referenced to create the fixture.
public class ApiWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration(config => {});
builder.ConfigureTestServices(services => {});
}
}
public class WeatherForecastControllerTests : IClassFixture<ApiWebApplicationFactory>
{
private readonly ApiWebApplicationFactory _fixture;
public WeatherForecastControllerTests(ApiWebApplicationFactory fixture)
{
_fixture = fixture;
}
[Fact]
public async Task GET_retrieves_weather_forecast()
{
}
}
The final step is to run the tests again and make sure they're all turning green.
Happy upgrading!
Follow me on Twitter at @tim_deschryver | Subscribe to the Newsletter | Originally published on timdeschryver.dev.
Top comments (0)