NuGet for unit testing ASP .NET Core middleware

NuGet for unit testing ASP .NET Core middleware

In last few articles, I have been trying to go through all the common and important aspects related to ASP .NET Core middleware.

If you are interested, below are the links to previous posts:

In previous post, I explained a simple approach which can be used to unit test the ASP .NET Core middlewares. In this post, let’s talk about an alternative approach using a NuGet Package.

Why?

In previous post, we have seen the approach which is outlined again as:

  • Mock all dependencies and Setup DI.
  • Instantiate the Middleware using new keyword
    • context: pass DefaultHttpContext object to it
    • next: pass a dummy terminal middleware to it to end the response
  • Assert the changes in DefaultHttpContext object

So, in the above approach, the middleware instance is required to be created using new operator. It does not make use of request process pipeline to instantiate the middleware.

But what if, you want to configure the request process pipeline similar to as in ASP .NET Core web application, which will have either one or more than one middleware in the pipeline ?

The NuGet Package

The Microsoft.AspNetCore.TestHost Package helps in this. It helps you to setup a lightweight TestServer for unit tests. The unit tests can create the web host and send the custom requests to the TestServer. This will mimic the same behavior of sending requests from external client.

The advantage obviously is better readable request pipeline, which contains one or more middleware components.

Is it really a unit test ?

But some of us may say that if we include multiple middleware in the request pipeline, would it still be called a unit test ? Well, people may have different opinions about what is the real meaning of the word unit from the concept of unit tests.

Some of us may say, unit for testing is class, some of us may say group of classes or some may say it is assembly and some may say the unit is a requirement.

I personally believe, a unit test should test a single requirement which is implemented by the design. The design, here means, a set of classes which implement a unit of requirement and they must be part of same single process.

So, a unit test is the test which tests a single implemented requirement. So, it may focus on single class or set of classes, but the asserts should always define the requirement and not the design.

The Middleware and Pipeline

For the demo purpose, we are going to use the same middleware which I used in the previous article. It adds a cookie to the response if it is not already present in the request.

public class DummyCookieMiddleware
{
private readonly RequestDelegate _next;
public DummyCookieMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext context)
{
// Check if cookie is already present
var dummyCookieValue = context.Request.Cookies["DummyCookie"];
var isValidDummyCookieValue = Guid.TryParse(dummyCookieValue, out Guid result);
// If not present then add new value
if(!isValidDummyCookieValue) {
context.Response.Cookies.Append("DummyCookie", Guid.NewGuid().ToString());
}
// handover request to next delegate in pipeline
return _next(context);
}
}

The request process pipeline is as shown below:

public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseDummyCookieMiddleware();
app.Run(async context =>
{
await context.Response.WriteAsync("App Response");
});
}
}
view raw Startup.cs hosted with ❤ by GitHub

The Test Project

Below is the set of .NET CLI commands to create the test project. It creates project, add reference of WebApp project to the test project. Then it adds the required NuGet package – the Microsoft.AspNetCore.TestHost Package – to the test project.

## Go to the directory which contains the web app project
cd D:\BLOG_Samples\FirstWebApp
## Navigate to parent directory of web app project directory
cd ..
## Create directory for test project
mkdir FirstWebApp.Tests
## Move to the test project directory
cd .\FirstWebApp.Tests\
## Create xUnit test proejct of same name (FirstWebApp.Tests)
dotnet new xunit
## Add reference of Web App project to the test project
dotnet add reference ..\FirstWebApp\FirstWebApp.csproj
## Add NuGet package
dotnet add package Microsoft.AspNetCore.TestHost
## Add NuGet package for hosting TestServer
dotnet add package Microsoft.Extensions.Hosting
## Move to the parent directory
cd ..
## Open Visual Studio Code
Code .
view raw commands.sh hosted with ❤ by GitHub

Setup

Below code snippet shows two classes:

  • Setup class, which initializes the TestServer instance using a request pipeline defined in TestStartup class.
  • TestStartup class, which defines the request pipeline required for tests. For the demo, the pipeline has only one middleware and a sample mock terminal middleware.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using FirstWebApp;
namespace FirstWebApp.Tests
{
public class Setup
{
public const string ExpectedOutput = "Request handed over to next request delegate";
public static async Task<TestServer> InitTestServer()
{
var host = await new HostBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder
.UseTestServer()
.UseStartup<TestStartup>();
})
.StartAsync();
return host.GetTestServer();
}
}
public class TestStartup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseDummyCookieMiddleware();
app.Run(async context =>
{
await context.Response.WriteAsync(Setup.ExpectedOutput);
});
}
}
}
view raw TestStartup.cs hosted with ❤ by GitHub

Test

The test class is simple. It just has a single unit test to check if cookie is added to the response. The unit test

using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Xunit;
namespace FirstWebApp.Tests
{
public class DummyCookieMiddlewareTests
{
[Fact]
public async Task GivenWebApp_WhenFirstRequestIsSentToWebApp_ThenDummyCookieIsAddedToResponse()
{
// Arrange
var memoryStream = new MemoryStream();
var testServer = await Setup.InitTestServer();
// Act
var resultContext = await testServer.SendAsync(context => {
context.Response.Body = memoryStream;
context.Request.Path = "/";
});
// Assert 1: if the next delegate was invoked
memoryStream.Position = 0;
string responseBody = new StreamReader(memoryStream).ReadToEnd();
Assert.Equal(Setup.ExpectedOutput, responseBody);
// Assert 2: if the cookie is added to the response
var setCookieHeaders = resultContext.Response.GetTypedHeaders().SetCookie;
var cookie = setCookieHeaders?.FirstOrDefault(x => x.Name == "DummyCookie");
Assert.True(Guid.TryParse(cookie.Value, out Guid result));
}
}
}

Note

There are few things which needs to be noted, before designing the unit tests using this package.

These requests are sent in memory so it does not test the serialization requirements. Also, the exceptions raised by the TestServer web host can be caught by the calling test.

As we have seen in above examples, it is possible with this NuGet Package to customize server data structures, such as HttpContext, directly in the test.

Also, this same NuGet package can be used for writing integration tests (or API tests).

I hope you found this information useful. Let me know your thoughts.

Leave a Reply