How can you unit test an Action Filter in ASP.NET Web Api?

.NetUnit Testingasp.net Web-ApiAction Filter

.Net Problem Overview


I was looking to add an Action Filter to my service to handle adding link data to the response message. I have found that I need to mock HttpActionExecutedContext but it's a difficult class to mock, how are you dealing with Action Filter testing?

.Net Solutions


Solution 1 - .Net

You can create a fake for HttpActionExecutedContext as below:

public static HttpActionContext CreateActionContext(HttpControllerContext controllerContext = null, HttpActionDescriptor actionDescriptor = null)
{
    HttpControllerContext context = controllerContext ?? ContextUtil.CreateControllerContext();
    HttpActionDescriptor descriptor = actionDescriptor ?? new Mock<HttpActionDescriptor>() { CallBase = true }.Object;
    return new HttpActionContext(context, descriptor);
}

public static HttpActionExecutedContext GetActionExecutedContext(HttpRequestMessage request, HttpResponseMessage response)
{
    HttpActionContext actionContext = CreateActionContext();
    actionContext.ControllerContext.Request = request;
    HttpActionExecutedContext actionExecutedContext = new HttpActionExecutedContext(actionContext, null) { Response = response };
    return actionExecutedContext;
}

I just copied and pasted that code from the ASP.NET Web API source code: ContextUtil class. Here is a few examples on how they tested some built in filters:

ActionFilterAttributeTest is the test class for ActionFilterAttribute which is an abstract class but you will get the idea.

Solution 2 - .Net

Just new one up.

private HttpActionContext CreateExecutingContext()
{
    return new HttpActionContext { ControllerContext = new HttpControllerContext {   Request = new HttpRequestMessage() } };
}

private HttpActionExecutedContext CreateExecutedContextWithStatusCode(HttpStatusCode statusCode)
{
    return new HttpActionExecutedContext
    {
        ActionContext = new HttpActionContext
        {
            ControllerContext = new HttpControllerContext
            {
                Request = new HttpRequestMessage()
            }
        },
        Response = new HttpResponseMessage
        {
            StatusCode = statusCode,
            Content = new StringContent("blah")
        }
    };
}

Solution 3 - .Net

I had the same problem when trying to test a custom unhandled exception filter I had built.

This did the trick. Lots of newing up and a very long line of code.

var httpActionExecutedContext = new HttpActionExecutedContext(
    new HttpActionContext(
        new HttpControllerContext(
            new HttpConfiguration(),
            Substitute.For<IHttpRouteData>(),
            new HttpRequestMessage()),
    Substitute.For<HttpActionDescriptor>()),
    null);

NSubstiute was used, but any mocking framework of your choice that handles abstract base classes would be fine.

Hope this helps

Solution 4 - .Net

I've been banging my head against a brick wall over this also. I tried contextUtil but kept getting a null reference exception. I found out how to call an actionFilter in this post N.B. The actionFilter wasn't being invoked when using a Mock instance of the filter, I had to use the real object. HTH

Specifically:

var httpActionContext = new HttpActionContext
{
    ControllerContext = new HttpControllerContext
    {
        Request = requestMessage
    }
};

//call filter
var filter = new FooFilter();
filter.OnActionExecuting(httpActionContext);

Solution 5 - .Net

Referencing https://stackoverflow.com/a/44447349/5547177

You can create an HTTPActionContext yourself with the following:

 _ctx = new HttpActionContext
        {
            ControllerContext = new HttpControllerContext()
            {
                Request = new HttpRequestMessage()
               
            }
        };
        _ctx.Request.Properties[System.Web.Http.Hosting.HttpPropertyKeys.HttpConfigurationKey] = new HttpConfiguration();

The trick is without the Request.Properties entry setting, it will show an error of:

> The request does not have an associated configuration object or the > provided configuration was null.

This might be an oversight on the part of the designers, as you can set an HTTPConfiguration in the HTTPActionContext constructor!

Solution 6 - .Net

Here's a working example from 2018 (.NET Framework 4.5.1). It uses an ExceptionFilterAttribute but it should be similar for other FilterAttributes.

[Test]
public void MyTest()
{
    var request = new HttpRequestMessage(HttpMethod.Get, new Uri("http://www.google.com"));
    var response = new HttpResponseMessage();

    // This next line is necessary to avoid the following error
    // if you call `context.Request.CreateResponse(...)` inside the filter:
    // System.InvalidOperationException: The request does not have an associated configuration object or the provided configuration was null.
    // Discovered from https://stackoverflow.com/a/44447355/3312114
    request.SetConfiguration(new HttpConfiguration());

    var context = ContextUtil.GetActionExecutedContext(request, response);
    
    _myFilter.OnException(context); // Execute your methods

    Assert.AreEqual(HttpStatusCode.InternalServerError, context.Response.StatusCode); // Make your assertions
}

Then just copy the ContextUtil class into your test project somewhere. @thomasb's comment on @tugberk's answer suggests the latest code is on Codeplex. While that comment was in 2014 so there may even be later code, the 2014 code worked for me (in Jan 2018) while the original linked code did not. I've copied the later version below for convenience. Just drop this into a new file.

internal static class ContextUtil
{
    public static HttpControllerContext CreateControllerContext(HttpConfiguration configuration = null, IHttpController instance = null, IHttpRouteData routeData = null, HttpRequestMessage request = null)
    {
        HttpConfiguration config = configuration ?? new HttpConfiguration();
        IHttpRouteData route = routeData ?? new HttpRouteData(new HttpRoute());
        HttpRequestMessage req = request ?? new HttpRequestMessage();
        req.SetConfiguration(config);
        req.SetRouteData(route);

        HttpControllerContext context = new HttpControllerContext(config, route, req);
        if (instance != null)
        {
            context.Controller = instance;
        }
        context.ControllerDescriptor = CreateControllerDescriptor(config);

        return context;
    }

    public static HttpActionContext CreateActionContext(HttpControllerContext controllerContext = null, HttpActionDescriptor actionDescriptor = null)
    {
        HttpControllerContext context = controllerContext ?? ContextUtil.CreateControllerContext();
        HttpActionDescriptor descriptor = actionDescriptor ?? CreateActionDescriptor();
        descriptor.ControllerDescriptor = context.ControllerDescriptor;
        return new HttpActionContext(context, descriptor);
    }

    public static HttpActionContext GetHttpActionContext(HttpRequestMessage request)
    {
        HttpActionContext actionContext = CreateActionContext();
        actionContext.ControllerContext.Request = request;
        return actionContext;
    }

    public static HttpActionExecutedContext GetActionExecutedContext(HttpRequestMessage request, HttpResponseMessage response)
    {
        HttpActionContext actionContext = CreateActionContext();
        actionContext.ControllerContext.Request = request;
        HttpActionExecutedContext actionExecutedContext = new HttpActionExecutedContext(actionContext, null) { Response = response };
        return actionExecutedContext;
    }

    public static HttpControllerDescriptor CreateControllerDescriptor(HttpConfiguration config = null)
    {
        if (config == null)
        {
            config = new HttpConfiguration();
        }
        return new HttpControllerDescriptor() { Configuration = config, ControllerName = "FooController" };
    }

    public static HttpActionDescriptor CreateActionDescriptor()
    {
        var mock = new Mock<HttpActionDescriptor>() { CallBase = true };
        mock.SetupGet(d => d.ActionName).Returns("Bar");
        return mock.Object;
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAaron FischerView Question on Stackoverflow
Solution 1 - .NettugberkView Answer on Stackoverflow
Solution 2 - .NetSam ShilesView Answer on Stackoverflow
Solution 3 - .Netwill websterView Answer on Stackoverflow
Solution 4 - .NetoutofcoolnamesView Answer on Stackoverflow
Solution 5 - .Net3DPrintScannerView Answer on Stackoverflow
Solution 6 - .NetSeafishView Answer on Stackoverflow