How to integration test exception handlers in ASP.NET Core
Global exception handling in ASP.NET Core is one of those features you often just set and forget. Unless you realize that your error log is awfully quiet, applications with failing or misconfigured exception handling can cause severe problems for your end users. In this post, I'll show you how to test your global exception middleware using NUnit and WebApplicationFactory.

Before we begin testing, let us start with some information about error handling. I already wrote a few posts about it on this blog, but since a lot has happened in recent ASP.NET Core versions, we'd better start from scratch.
ASP.NET Core provides two layers for error handling:
- The Developer Exception Page for development (shows stack traces, headers, etc.).
- Exception Handler Middleware via
UseExceptionHandler
for non-development environments.
You have probably already seen code similar to this, which shows both features:
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
When on Development (your machine), we show the developer exception page (some call it the White Screen of Death) using the UseDeveloperExceptionPage
method when an error happens. When in another environment, like production, we show the content of the /Error
endpoint using the UseExceptionHandler
method.
In the example above, you are expected to implement the /Error
endpoint, but a simple approach that some prefer is implementing the error handling code directly in the UseExceptionHandler
method using one of the overloads:
app.UseExceptionHandler(appBuilder =>
{
appBuilder.Run(async context =>
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = 500;
var json = JsonSerializer.Serialize(new { message = "Generic error" });
await context.Response.WriteAsync(json);
});
});
The inline lambda approach is great for quick setups. To improve testability and separation of concerns, the code can be written using a custom error handler implementing the IExceptionHandler
interface:
public class MyExceptionHandler : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
{
httpContext.Response.ContentType = "application/json";
httpContext.Response.StatusCode = 500;
var json = JsonSerializer.Serialize(new { message = "Generic error" });
await httpContext.Response.WriteAsync(json, cancellationToken);
return true;
}
}
The MyExceptionHandler
class needs to be configured in the Program.cs
file like this:
builder.Services.AddExceptionHandler<MyExceptionHandler>();
// ...
app.UseExceptionHandler(_ => {});
For the rest of this post, I will use the exception handler in a separate class approach and write a unit test of that. I have seen several attempts to write tests of the exception handler class directly. And while that may be a good idea, it requires some boilerplate code to mock the HttpContext
. Also, testing the class directly doesn't test that the handler is correctly set up and works for failing endpoints.
Let's start by implementing a simple ASP.NET Core application based on the minimal API approach:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddExceptionHandler<MyExceptionHandler>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(_ => {});
}
app.MapGet("", () =>
{
return "Hello Dude!";
});
app.MapGet("/throw", () =>
{
throw new Exception("App crashed");
});
await app.RunAsync();
public partial class Program { }
The application configures the MyExceptionHandler
class already specified. Notice how the exception handler is only configured when the environment is not Development. I also declare two endpoints: /
and /throw
. The /
endpoint will trigger when requesting the front page and /throw
will throw an exception for us to use in the test. Finally, I include a partial class named Program
which is there for the test project to see the Program
class. You can use the InternalsVisibleTo
attribute instead, but I won't go into more details about that here.
To test that our custom exception handler is triggered when requesting an endpoint causing an exception, I'll create a new class library project and install the following NuGet packages:
dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package Microsoft.NET.Test.Sdk
dotnet add package NUnit
dotnet add package NUnit3TestAdapter
The Microsoft.AspNetCore.Mvc.Test
package includes test helpers for integration testing ASP.NET Core applications, which we will use shortly. The other three packages are NUnit itself as well as tools needed to run tests in Visual Studio.
The test code will be implemented in the following test method:
public class ExceptionHandlerTest
{
[Test]
public async Task CanGenerateErrorFromExceptionHandler()
{
}
}
We'll start by spinning up our ASP.NET Core application using the following code:
var factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.UseEnvironment("Production");
});
The WebApplicationFactory
class is a helper from the Microsoft.AspNetCore.Mvc.Testing
package we just installed. It spins up your ASP.NET Core app in-memory, without starting a real web server, and gives you an HttpClient
to send requests against it. To make sure that our exception handler is installed and not the developer exception page (remember the if
statement in the Program.cs
file), I use the WithWebHostBuilder
method to set the environment to Production. Notice how the Program
class from our web project is used as a generic, which makes WebApplicationFactory
class all of the normal initialization code needed for the real website (including our custom exception handler).
As already mentioned, the WebApplicationFactory
class provides helpful HttpClient
already configured to make requests against our in-memory website:
var client = factory.CreateClient();
var response = await client.GetAsync("/throw");
The GetAsync
method will invoke the /throw
endpoint that we implemented to throw an exception. The final piece missing in the test is checking the response to make sure that the exception handler triggered:
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.InternalServerError));
Assert.That(response.Content.Headers.ContentType?.MediaType, Is.EqualTo("application/json"));
var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Assert.That(json.RootElement.GetProperty("message").GetString(), Is.EqualTo("Generic error"));
The code asserts both the status code and the body, which is only generated by the exception handler.
Here's the full test:
[Test]
public async Task CanGenerateErrorFromExceptionHandler()
{
var factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.UseEnvironment("Production");
});
var client = factory.CreateClient();
var response = await client.GetAsync("/throw");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.InternalServerError));
Assert.That(response.Content.Headers.ContentType?.MediaType, Is.EqualTo("application/json"));
var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Assert.That(json.RootElement.GetProperty("message").GetString(), Is.EqualTo("Generic error"));
}
That's it. With just a few helpers from ASP.NET Core's testing libraries, we launched an in-memory web app and verified that our exception handler behaves as expected. For more complex exception handler logic, you can consider writing unit tests of the handler directly. As long as you back it up by writing an integration test of the configuration part too.
elmah.io: Error logging and Uptime Monitoring for your web apps
This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.
See how we can help you monitor your website for crashes Monitor your website