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().
Table of Contents
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)