Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I test for the presence of an Action Filter with constructor arguments?

I am trying to test that my base controller is decorated with a certain action filter. Because this filter's constructor looks to web.config, my first try at testing fails because the test project doesn't have a valid config file. Moving on, I used a TestConfigProvider that I inject into the filter constructor, but the following test fails because the config provider isn't passed to the constructor. How else can I test if this filter is applied?

[TestMethod]
public void Base_controller_must_have_MaxLengthFilter_attribute()
{
    var att = typeof(BaseController).GetCustomAttribute<MaxLengthFilter>();
    Assert.IsNotNull(att);
}
like image 661
ProfK Avatar asked Dec 02 '14 08:12

ProfK


1 Answers

Well, you have taken a good first step by recognizing that Web.config is just another dependency and wrapping it into a ConfigProvider to inject is an excellent solution.

But, you are getting tripped up on one of the design problems of MVC - namely, that to be DI-friendly, attributes should only provide meta-data, but never actually define behavior. This isn't an issue with your approach to testing, it is an issue with the approach to the design of the filter.

As pointed out in the post, you can get around this issue by splitting your action filter attribute into 2 parts.

  1. An attribute that contains no behavior to flag your controllers and action methods with.
  2. A DI-friendly class that implements IActionFilter and contains the desired behavior.

The approach is to use the IActionFilter to test for the presence of the attribute, and then execute the desired behavior. The action filter can be supplied with all dependencies and then injected when the application is composed.

IConfigProvider provider = new WebConfigProvider();
IActionFilter filter = new MaxLengthActionFilter(provider);
GlobalFilters.Filters.Add(filter);

NOTE: If you need any of the filter's dependencies to have a lifetime shorter than singleton, you will need to use a GlobalFilterProvider as in this answer.

The implementation of MaxLengthActionFilter would look something like this:

public class MaxLengthActionFilter : IActionFilter
{
    public readonly IConfigProvider configProvider;

    public MaxLengthActionFilter(IConfigProvider configProvider)
    {
        if (configProvider == null)
            throw new ArgumentNullException("configProvider");
        this.configProvider = configProvider;
    }

    public void OnActionExecuted(ActionExecutedContext filterContext)
    {
        var attribute = this.GetMaxLengthAttribute(filterContext.ActionDescriptor);
        if (attribute != null)
        {
            var maxLength = attribute.MaxLength;

            // Execute your behavior here, and use the configProvider as needed
        }
    }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var attribute = this.GetMaxLengthAttribute(filterContext.ActionDescriptor);
        if (attribute != null)
        {
            var maxLength = attribute.MaxLength;

            // Execute your behavior here, and use the configProvider as needed
        }
    }

    public MaxLengthAttribute GetMaxLengthAttribute(ActionDescriptor actionDescriptor)
    {
        MaxLengthAttribute result = null;

        // Check if the attribute exists on the controller
        result = (MaxLengthAttribute)actionDescriptor
            .ControllerDescriptor
            .GetCustomAttributes(typeof(MaxLengthAttribute), false)
            .SingleOrDefault();

        if (result != null)
        {
            return result;
        }

        // NOTE: You might need some additional logic to determine 
        // which attribute applies (or both apply)

        // Check if the attribute exists on the action method
        result = (MaxLengthAttribute)actionDescriptor
            .GetCustomAttributes(typeof(MaxLengthAttribute), false)
            .SingleOrDefault();

        return result;
    }
}

And, your attribute which should not contain any behavior should look something like this:

// This attribute should contain no behavior. No behavior, nothing needs to be injected.
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
public class MaxLengthAttribute : Attribute
{
    public MaxLengthAttribute(int maxLength)
    {
        this.MaxLength = maxLength;
    }

    public int MaxLength { get; private set; }
}

With a more loosely coupled design, testing for the existence of the attribute is much more straightforward.

[TestMethod]
public void Base_controller_must_have_MaxLengthFilter_attribute()
{
    var att = typeof(BaseController).GetCustomAttribute<MaxLengthAttribute>();
    Assert.IsNotNull(att);
}
like image 169
NightOwl888 Avatar answered Oct 19 '22 02:10

NightOwl888