ASP.NET Core – How to unit test an action filter

To unit test an action filter, you have to pass in an action filter context object (which requires a lot of setup). Action filter methods are void, so you have to verify the behavior by inspecting the context object (or dependencies, like a logger, if you are injecting those).

Here’s an example of doing the bare minimum setup to unit test an action filter method:

//Bare minimum usings you need for setting up the filter context
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;


//arrange
var actionFilter = new CustomActionFilter();

//Bare minimum for setting up filter context
var httpContext = new DefaultHttpContext();
var actionContext = new ActionContext(httpContext,
	new RouteData(),
	new ActionDescriptor(),
	new ModelStateDictionary());
var actionExecutingContext = new ActionExecutedContext(actionContext,
	new List<IFilterMetadata>(),
	controller: null);

//act
actionFilter.OnActionExecuted(actionExecutingContext);

//assert
var contentResult = actionExecutingContext.Result as ContentResult;
Assert.AreEqual((int)System.Net.HttpStatusCode.BadRequest, contentResult.StatusCode);
Code language: C# (cs)

The good thing is you can use the defaults for all of the filter context’s dependencies. You may need to do a little additional setup depending on what your action filter is doing and what you want to test.

In this article, I’ll show examples of unit testing three action filters methods – OnActionExecuting(), OnActionExecuted(), and OnActionExecutionAsync().

Example of unit testing OnActionExecuting()

Code

The following action filter verifies that a required request header was sent and returns a BadResult (400 status code) if it’s missing.

public class RequireCustomHeader : ActionFilterAttribute
{
	private readonly string RequiredHeader;
	public RequireCustomHeader(string requiredHeader)
	{
		RequiredHeader = requiredHeader;
	}
	public override void OnActionExecuting(ActionExecutingContext context)
	{
		if (!context.HttpContext.Request.Headers.ContainsKey(RequiredHeader))
		{
			context.Result = new ContentResult()
			{
				StatusCode = (int)System.Net.HttpStatusCode.BadRequest,
				Content = $"Missing required header - {RequiredHeader}"
			};
		}
	}
}
Code language: C# (cs)

Unit Test

If the required header is missing from the request, then the action filter should set the result on the filter context to BadRequest (400).

To unit test this, first do the bare minimum setup to create the filter context object and pass it into OnActionExecuting(). To actually verify the behavior, we have to inspect the filter context to make sure the code set the result to a 400 status code.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;

[TestMethod()]
public void TestRequireCustomHeader_WhenHeaderMissing_ReturnsBadRequest()
{
	//arrange
	var requiredHeader = "Test";
	var actionFilter = new RequireCustomHeader(requiredHeader);

	var httpContext = new DefaultHttpContext();
	var actionContext = new ActionContext(httpContext, 
		new RouteData(), 
		new ActionDescriptor(), 
		new ModelStateDictionary());
	var actionExecutingContext = new ActionExecutingContext(actionContext, 
		new List<IFilterMetadata>(), 
		new Dictionary<string, object>(), 
		controller: null);

	//act
	actionFilter.OnActionExecuting(actionExecutingContext);

	//assert
	var contentResult = actionExecutingContext.Result as ContentResult;
	Assert.AreEqual((int)System.Net.HttpStatusCode.BadRequest, contentResult.StatusCode);
}
Code language: C# (cs)

Example of unit testing OnActionExecuted()

Code

The following action filter adds a custom response header.

public class AddDebugInfoToResponse : ActionFilterAttribute
{
	public override void OnActionExecuted(ActionExecutedContext context)
	{
		context.HttpContext.Response.Headers.Add("DebugInfo", context.ActionDescriptor.DisplayName);

		base.OnActionExecuted(context);
	}
}
Code language: C# (cs)

Unit Test

When the action filter is called, it should add the custom header with the ActionDescriptor’s display name.

To unit test this, start with the bare minimum setup to create the appropriate filter context (note: ActionExecutedContext instead of ActionExecutingContext). Since the action filter is using ActionDescriptor.DisplayName, initialize it to something. Pass the filter context to OnActionExecuted() and assert the response header has the expected custom header.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;


[TestMethod()]
public void OnActionExecutedTest_AddsResponseHeader()
{
	//arrange
	var actionFilter = new AddDebugInfoToResponse();
	var expectedHeaderValue = "Test";

	var httpContext = new DefaultHttpContext();
	var actionContext = new ActionContext(httpContext,
		new RouteData(),
		new ActionDescriptor() { DisplayName = expectedHeaderValue },
		new ModelStateDictionary());
	var actionExecutedContext = new ActionExecutedContext(actionContext,
		new List<IFilterMetadata>(),
		controller: null);

	//act
	actionFilter.OnActionExecuted(actionExecutedContext);

	//assert
	Assert.IsTrue(httpContext.Response.Headers.ContainsKey("DebugInfo"), "Missing header");
	Assert.AreEqual(expectedHeaderValue, httpContext.Response.Headers["DebugInfo"].ToString());
}
Code language: C# (cs)

This is an example of needing to do a little extra setup based on what the action filter is using.

Example of unit testing OnActionExecutionAsync()

Code

The following action filter is measuring the execution time of the action method and sticking the elapsed time in a custom response header.

public class LogStats : ActionFilterAttribute
{
	public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
	{
		var stopwatch = Stopwatch.StartNew();

		var actionExecutedContext = await next();

		stopwatch.Stop();

		actionExecutedContext.HttpContext.Response.Headers.Add("Stats", stopwatch.Elapsed.ToString());
	}
}
Code language: C# (cs)

Unit Test

The test scenario is straightforward: verify that the action filter adds a custom response header.

OnActionExecutionAsync() requires more setup than the other methods because 1) it’s async 2) you have to pass in the ActionExecutionDelegate parameter 3) you have to setup an ActionExecutingContext and an ActionExecutedContext.

Here’s how to unit test in this async method (note: I highlighted the parts of the setup that are unique to unit testing this):

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;


[TestMethod()]
public async Task LogStatsTest_AddsStatsResponseHeader()
{
	//arrange
	var actionFilter = new LogStats();

	var httpContext = new DefaultHttpContext();
	var actionContext = new ActionContext(httpContext,
		new RouteData(),
		new ActionDescriptor(),
		new ModelStateDictionary());
	var actionExecutingContext = new ActionExecutingContext(actionContext,
		new List<IFilterMetadata>(),
		new Dictionary<string, object>(),
		controller: null);

	ActionExecutionDelegate mockDelegate = () => {
		return Task.FromResult(new ActionExecutedContext(actionContext, new List<IFilterMetadata>(), null));
	};

	//act
	await actionFilter.OnActionExecutionAsync(actionExecutingContext, mockDelegate);

	//assert
	Assert.IsTrue(httpContext.Response.Headers.ContainsKey("Stats"), "Missing header");
	StringAssert.Matches(httpContext.Response.Headers["Stats"].ToString(), new Regex("[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]+"));
}
Code language: C# (cs)

Leave a Comment