18

I'm looking to test an ActionFilterAttribute in a .NET Core 2.0 API project and wondering the best way to go about it. Note, I'm not trying to test this through a controller action, merely test the ActionFilterAttribute itself.

How might I go about testing this:

    public class ValidateModelAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            if (!context.ModelState.IsValid)
            {
                context.Result = new BadRequestObjectResult(context.ModelState);
            }
        }
    }
ovation22
  • 711
  • 2
  • 7
  • 19

2 Answers2

44

Create an instance of the context pass it to the filter and assert the expected behavior

For example

[TestClass]
public class ValidateModelAttributeTest {
    [TestMethod]
    public void Invalid_ModelState_Should_Return_BadRequestObjectResult() {
        //Arrange
        var modelState = new ModelStateDictionary();
        modelState.AddModelError("", "error");
        var httpContext = new DefaultHttpContext();
        var context = new ActionExecutingContext(
            new ActionContext(
                httpContext: httpContext,
                routeData: new RouteData(),
                actionDescriptor: new ActionDescriptor(),
                modelState: modelState
            ),
            new List<IFilterMetadata>(),
            new Dictionary<string, object>(),
            new Mock<Controller>().Object);

        var sut = new ValidateModelAttribute();

        //Act
        sut.OnActionExecuting(context);

        //Assert
        context.Result.Should().NotBeNull()
            .And.BeOfType<BadRequestObjectResult>();
    }
} 
Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • 3
    Thanks for the response. Found this after accepting that helped as well: https://stackoverflow.com/questions/36629391/how-to-mock-actionexecutingcontext-with-moq – ovation22 Sep 11 '17 at 19:38
  • @nkosi Quick q: when I try the above code, the `ModelState` is VALID. Is there a trick to get the modelState to be in an invalid state? Do we need to create some fake route or something? – Pure.Krome Mar 17 '18 at 10:32
  • 1
    @Pure.Krome you would need to add a model error to the model state dictionary to make it invalid. – Nkosi Mar 18 '18 at 22:12
  • I'm only getting "null" back , any suggestions why? – Frank R. Haugen Mar 19 '20 at 10:53
  • @FrankR.Haugen which part are you getting null – Nkosi Mar 19 '20 at 10:59
  • @Nkosi context.Result.Should().NotBeNull() is always false – Frank R. Haugen Mar 19 '20 at 11:01
  • @FrankR.Haugen If you have a condition that depends on model state then you would need to add a model error to the model state dictionary so that is is invalid and would enter your conditional statement – Nkosi Mar 19 '20 at 11:03
  • @Nkosi I added this before I saw your answer-comment: [Link to question](https://stackoverflow.com/questions/60755839/how-to-unit-test-actionfilterattributes-onactionexecuting-with-xunit?noredirect=1&lq=1) (So you know my code) – Frank R. Haugen Mar 19 '20 at 11:08
  • @Nkosi, Thanks, your posts always seems to find a way to help me – johnny 5 Apr 24 '20 at 13:46
1

Here is a real life example, where I also access the method info and params inside the action filter attribute:

Suppose I have a Controller Method with ActionAttribute like this:

 public class HomeController : Controller
    {
    ...
    [FeatureFlagActionAtrribute("user", new String[] { "Feature1" })]
    public IActionResult DoSomethingWithFilterAction(String user)
        {...}
    }

The http call would be something like this:

/Home/DoSomethingWithFilterAction?user="user1"

Now, I want to test the ActionAttribute FeatureFlagActionAtrribute in such a context.

If you would apply above suggestion to this example, it would look like this (worked for me, at least)

 var methodInfoDoSomethingWithFilterAction = typeof(HomeController).GetMethod(nameof(HomeController.DoSomethingWithFilterAction));
    var httpContext = new DefaultHttpContext();
    var routeData = new RouteData();
    FeatureFlagActionAtrribute FeatureFlagActionAtrributeFilter = methodInfoDoSomethingWithFilterAction.GetCustomAttribute<FeatureFlagActionAtrribute>();
    ActionDescriptor actionDescriptor = new ControllerActionDescriptor()
                {
                    ActionName = methodInfoDoSomethingWithFilterAction.Name,
                    ControllerName = typeof(FeatureFlagTest).Name,
                    DisplayName = methodInfoDoSomethingWithFilterAction.Name,
                    MethodInfo = methodInfoDoSomethingWithFilterAction,
                };

    ActionContext actionContext = new ActionContext(httpContext, routeData, actionDescriptor) ;
    var homeController = new HomeController();
    var attribute = new FeatureFlagActionAtrribute("user", new string[] { "feature1" });
    IDictionary<string, object> actionArguments = new Dictionary<string, object>
                {
                    ["user"] = "user1"
                };

    var filterMetadata = new List<IFilterMetadata>() { featureFlagActionAtrributeFilter };

    ActionExecutingContext actionExecutedContext = new 
    ActionExecutingContext(actionContext, filterMetadata, actionArguments, homeController);


    attribute.OnActionExecuting(actionExecutedContext);

Then inside the ActionFilterAttribute:

public override void OnActionExecuting(ActionExecutingContext context)
{
        ControllerActionDescriptor actionDescriptor = (ControllerActionDescriptor)context.ActionDescriptor;
        Debug.Print($"2. @Before Method called {actionDescriptor.ControllerName}Controller.{actionDescriptor.ActionName}");
        var controllerName = actionDescriptor.ControllerName;
        var actionName = actionDescriptor.ActionName;
        IDictionary<object, object> properties = actionDescriptor.Properties;
        ParameterInfo[] paramsOfMethod = actionDescriptor.MethodInfo.GetParameters();
        var fullName = actionDescriptor.DisplayName;

        var paramNameForKeyOfFeature = ParamNameForKeyOfFeature;

        var arguments = context.ActionArguments;
Punit Vora
  • 5,052
  • 4
  • 35
  • 44
Roland Roos
  • 1,003
  • 10
  • 4