views:

342

answers:

4

What is the best way to unit test the controller action HttpAcceptAttribute verbs?

So far I have the following but it's so ugly even a mother couldn't love it and not very flexible. Is there a better way?

[Fact] // using xUnit, mocking controller in class
public void FilterControllerTestRemoveFilterByProductAttributeIsOfTypePost()
{
    Type[] paramTypes = new[] { typeof(int) };
    var method = typeof(FilterController).GetMethod("MyMethod", paramTypes);

    var attributes = method.GetCustomAttributes(typeof(AcceptVerbsAttribute), false).Cast<AcceptVerbsAttribute>().SingleOrDefault();
    Assert.NotNull(attributes);
    Assert.Equal(1, attributes.Verbs.Count());
    Assert.True(attributes.Verbs.First().Equals(HttpVerbs.Post.ToString(), StringComparison.InvariantCultureIgnoreCase));
}

Thanks Mac

+1  A: 
using System;
using System.Linq;
using System.Reflection;
using System.Web.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Unknown.Tests
{

    public static class MvcAssert
    {

        public static MemberInfo[] HasPostAction(Controller controller, string actionName, int expectedCount)
        {
            if (controller == null)
                throw new ArgumentNullException("controller");

            if (string.IsNullOrEmpty(actionName))
                throw new ArgumentNullException("actionName");

            MemberInfo[] members = controller.GetType().FindMembers(
                MemberTypes.Method,
                BindingFlags.Public | BindingFlags.Instance,
                (m, c) => (m.Name == actionName && m.IsDefined(typeof(AcceptVerbsAttribute), false) && ((AcceptVerbsAttribute)Attribute.GetCustomAttribute(m, typeof(AcceptVerbsAttribute))).Verbs.Any((v) => v.Equals("Post", StringComparison.InvariantCultureIgnoreCase))),
                null);

            Assert.AreEqual<int>(expectedCount, members.Length);

            return members;
        }

    }

}

Using

public void FilterControllerTestRemoveFilterByProductAttributeIsOfTypePost()
{
    FilterController controller = new FilterController();
    MvcAssert.HasPostAction(controller, "RemoveFilterByProduct", 1);
}
Mehdi Golchin
That looks good. ThanksRefining the idea even further though, do you think there is a way to make it even more generic to allow an array of HttpVerbs to be passed in and have the method Assert only the HttpVerbs are an exact match or is that overkill and seperate methods for post, get, delete, put or combinations of them is enough?Ideally I'd like to be able to call it something like thisMvcAssert.HasHttpVerbs(controller, actionName, paramTypes, HttpVerbs[]);
Mac
+1  A: 
using System;
using System.Linq;
using System.Reflection;
using System.Web.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MvcApplication4.Tests
{

    public static class MvcAssert
    {

        public static MethodInfo ActionExists(Controller controller, string actionName, HttpVerbs expectedVerbs, params Type[] paramTypes)
        {
            if (controller == null)
                throw new ArgumentNullException("controller");

            if (string.IsNullOrEmpty(actionName))
                throw new ArgumentNullException("actionName");

            int actualVerbs = 0;

            MethodInfo action = controller.GetType().GetMethod(actionName, paramTypes);
            Assert.IsNotNull(action, string.Format("The specified action '{0}' could not be found.", actionName));

            AcceptVerbsAttribute acceptVerb = Attribute.GetCustomAttribute(action, typeof(AcceptVerbsAttribute)) as AcceptVerbsAttribute;

            if (acceptVerb == null)
                actualVerbs = (int)HttpVerbs.Get;
            else
                actualVerbs = (int)Enum.Parse(typeof(HttpVerbs), string.Join(", ", acceptVerb.Verbs.ToArray()), true);

            Assert.AreEqual<int>(actualVerbs, (int)expectedVerbs);

            return action;
        }

    }

}
Mehdi Golchin
Superb, thanks Mehdi
Mac
+1  A: 

No reflection and magic strings, easy to rename controller and method without breaking the unit test:

[TestMethod]
public void HomeController_Index_Action_Should_Accept_Post_Verb_Only()
{
    Expression<Action<HomeController>> expression = (HomeController c) => c.Index(null);
    var methodCall = expression.Body as MethodCallExpression;
    var acceptVerbs = (AcceptVerbsAttribute[])methodCall.Method.GetCustomAttributes(typeof(AcceptVerbsAttribute), false);
    acceptVerbs.ShouldNotBeNull("");
    acceptVerbs.Length.ShouldBe(1);
    acceptVerbs[0].Verbs.First().ShouldBe("POST");
}
Darin Dimitrov
Interesting, very clean approach, many thanks
Mac
A: 

A bit modified version Darin`s solution.

  [Fact]
  public void Delete_Verb(){
    VerifyVerb<HttpDeleteAttribute>(x=>x.Delete(null));
  }

  protected void VerifyVerb<TVerbType>(Expression<Action<T>> exp){
      var methodCall = exp.Body as MethodCallExpression;
      var acceptVerbs = methodCall.Method
        .GetCustomAttributes(typeof(TVerbType), false);
      acceptVerbs.Should().Not.Be.Null();
      acceptVerbs.Length.Should().Be(1);
  }
Arnis L.