The ASP.NET testing story has gotten easier with each new iteration, emphasizing dependency injection and inversion of control (IoC) containers contributing much of the plumbing for your web applications. Along with more accessible configuration options, we also see more opportunities in ASP.NET Core to access our web application in unit testing scenarios, easing the burden of setting up integration tests. Generally, it has been a positive advancement in the .NET space, which hopefully will lead to better-tested code and fewer bugs in production.
In this post, we’ll go through the steps of setting up an ASP.NET Core web application using minimal APIs/minimal hosting to work with a unit testing library. In this case, we’ll be using XUnit, but this post can be adapted to work with your testing library of choice.
The Web Application Project
You’ll need to start with a straightforward minimal API project. This sample will have a single endpoint with two service dependencies: IMessageService
and NameService
.
public interface IMessageService
{
string SayHello();
}
public class MessageService : IMessageService
{
public string SayHello() => "Hello, World!";
}
public class NameService
{
public string Name => "Khalid";
}
We’ll be using Minimal Hosting, where our Program
class will be generated. It’s important to understand that the Program
class will be declared internal
. Later, you’ll need to modify the csproj
of your web application to allow your test project access to the Program
type.
Now, you’ll need to create our application. You can put all of your code in our Program.cs
class in this example, along with our services.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IMessageService, MessageService>();
builder.Services.AddScoped<NameService>();
var app = builder.Build();
app.MapGet("/", (IMessageService message, NameService names)
=> $"{names.Name} says \"{message.SayHello()}\"");
app.Run();
public interface IMessageService
{
string SayHello();
}
public class MessageService : IMessageService
{
public string SayHello() => "Hello, World!";
}
public class NameService
{
public string Name => "Khalid";
}
Notice that one of the services, MessageService
, implements an interface, while NameService
does not. Accordingly, you’ll be replacing the IMessageService
dependency in a test while still utilizing the NameService
in its current implementation.
Finally, let’s make sure that the Program
class is visible to your unit testing project. In your csproj
, add the InternalsVisibleTo
element, making sure the Include
attribute has the assembly name of your test project. Be sure to change the value according to your assembly names.
<ItemGroup>
<InternalsVisibleTo Include="TestingWebRequests.Tests" />
</ItemGroup>
Now we’re ready to write some tests!
The Unit Test Project
You’ll first need to create a unit testing project. As mentioned above, this post uses XUnit, but the steps shown here will work with NUnit and other testing frameworks.
First, you’ll need to add the Microsoft.AspNetCore.Mvc.Testing
package. This library holds the WebApplicationFactory<>
class, which will allow us to configure our web application under testing conditions.
dotnet add package Microsoft.AspNetCore.Mvc.Testing
Now that you’ve added the package to your unit testing library, you’ll need to create an implementation of WebApplicationFactory<>
. In this case, you’ll create a TestApplication
class. You’ll also need a new implementation of the IMessageService
.
// internal is important as it's the
// same access level as `Program`
internal class TestApplication : WebApplicationFactory<Program>
{
public string Message { get; set; }
protected override IHost CreateHost(IHostBuilder builder)
{
builder.ConfigureServices(s => {
s.AddScoped<IMessageService, TestMessageService>(
_ => new TestMessageService {
Message = Message
});
});
return base.CreateHost(builder);
}
}
public class TestMessageService : IMessageService
{
/// <summary>
/// Allow us to set the message
/// </summary>
public string Message { get; set; } = "Hello, World!";
public string SayHello() => Message;
}
Notice how the base implementation of TestApplication
is WebApplicationFactory<Program>
. The use of the type here is why you changed the visibility of the Program
class. The library uses the marker class to get the assembly reference and read any previous code configuration.
Now, let’s write a test!
public class RootEndpointTests
{
private readonly ITestOutputHelper output;
private readonly TestApplication app;
public RootEndpointTests(ITestOutputHelper output)
{
this.output = output;
app = new TestApplication();
}
[Fact]
public async Task Can_get_message()
{
app.Message = "test message";
var client = app.CreateDefaultClient();
var result = await client.GetStringAsync("/");
output.WriteLine(result);
Assert.Equal($"Khalid says \"{app.Message}\"", result);
}
}
In the code above, you change the IMessageService
implementation to one in which you can modify the message
. In the case of NamedService
, you want to continue to use the implementation in the original configuration.
Let’s break down the test method into its most essential components:
- Creating our
TestApplication
instance on each test run. - Set our
Message
as a property of our test application. - Get an in-memory
HttpClient
to call the root endpoint. - Call the root endpoint and store the
result
. - Assert the
result
is correct.
The final code looks like this:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Xunit;
using Xunit.Abstractions;
namespace TestingWebRequests.Tests;
public class RootEndpointTests
{
private readonly ITestOutputHelper output;
private readonly TestApplication app;
public RootEndpointTests(ITestOutputHelper output)
{
this.output = output;
app = new TestApplication();
}
[Fact]
public async Task Can_get_message()
{
app.Message = "test message";
var client = app.CreateDefaultClient();
var result = await client.GetStringAsync("/");
output.WriteLine(result);
Assert.Equal($"Khalid says \"{app.Message}\"", result);
}
}
internal class TestApplication : WebApplicationFactory<Program>
{
public string Message { get; set; }
protected override IHost CreateHost(IHostBuilder builder)
{
builder.ConfigureServices(s => {
s.AddScoped<IMessageService, TestMessageService>(
_ => new TestMessageService {
Message = Message
});
});
return base.CreateHost(builder);
}
}
public class TestMessageService : IMessageService
{
/// <summary>
/// Allow us to set the message
/// </summary>
public string Message { get; set; } = "Hello, World!";
public string SayHello() => Message;
}
Conclusion
I’m thrilled with the test integration story in ASP.NET Core, and as this post demonstrated, it takes very little code to write and test any existing ASP.NET Core application. Using the example here, you could replace your database, third-party services, or other dependencies with relative ease. I hope you found this post helpful, and please share it with friends and coworkers.