UK based .Net dude

Tuesday, 20 May 2008

RouteData -> Controller.Action(params) validation


Following on from Phil Haack's excellent route evaluator for unit testing routes article, I got to wondering that if in addition to testing that for the presence of a RouteData match in our unit tests if we could also test that the invoked method and it's supplied parameters meet our expectations too.

To that end I have created an extension method for the RouteData class called VerifyAction which takes the generic parameter of the type of controller that we expect to be invoked and a lambda expression for a method call that returns an ActionResult value.

As with any extension method remember to include the namespace in your using clauses to make it available to the compiler

The specified lambda expression will take the parameters that should have been extracted from the virtualPath or supplied by the routes defaults.

An example is probably in order:

Example Unit Test

public void TestHomeControllerActionWithIdParam()
{
var routes = new RouteCollection();
GlobalApplication.RegisterRoutes(routes);

var evaluator = new RouteEvaluator(routes);
IList<RouteData> matchingRouteData = evaluator.GetMatches("~/Home/ActionWithIdParam/6");
Assert.IsTrue(matchingRouteData.Count > 0);
matchingRouteData[0].VerifyAction<HomeController>(c => c.ActionWithIdParam(6));
}


My expectation is that the url '~/Home/ActionWithIdParam/6' should invoke the ActionWithIdParam method found on HomeController passing in an Id of 6.



Validation Checks



The VerifyAction method checks the following aspects to determine if our expectations are met:




  • That the RouteData.Values["Controller"] value is equal to the Controller class supplied (minus the controller extension)


  • That the RouteData.Values["Action"] value is equal to the method name extracted from the lambda expression


  • That each argument of the action method has a corresponding entry in the RouteData.Values dictionary


  • That a null value is not supplied for an action method argument that expects a value type (eg int etc)


  • That each supplied argument value can be converted to the expected argument type


  • That each supplied argument value equals the expected argument value



If any of the checks fail an exception is thrown detailing the problem.



Feedback



Obviously that is what I intend that the code should do and hopefully it does :-) Seriously though when you find a bug, make an improvement or jump to the conclusion that the approach is flawed please can you post in the comments so that I can reap the benefits too, many thanks in advance.



The code for the extension method can be found below.



Thanks for stopping by,

Paul Blamire




RouteDataEvaluator Code



using System;
using System.Collections.Generic;
using System.Web.Routing;
using System.Web;
using System.Reflection;
using System.Linq.Expressions;

namespace MyNameSpace
{

public static class RouteDataEvaluator
{

public class RouteDataActionException : Exception
{
public RouteDataActionException(string message)
: base(message)
{

}
}

public static bool VerifyAction<CT>(this RouteData data, Expression<Func<CT, System.Web.Mvc.ActionResult>> expr) where CT : System.Web.Mvc.Controller
{

const string controllerKey = "Controller";
const string actionKey = "Action";

bool result = true;

LambdaExpression lambda = expr as LambdaExpression;
MethodCallExpression methodCall = lambda.Body as MethodCallExpression;

string cName = methodCall.Object.Type.Name;
string cNameLower = cName.ToLower();

if (SafeString(data.Values[controllerKey]) != ((cNameLower.EndsWith("controller")) ? cName.Substring(0, cName.Length - 10) : cName))
{
throw new RouteDataActionException(string.Format("Expected controller '{0}', matched '{1}'", cName, data.Values[controllerKey]));
}


if (SafeString(data.Values[actionKey]) != methodCall.Method.Name)
{
throw new RouteDataActionException(string.Format("Expected action '{0}', matched '{1}'", methodCall.Method.Name, data.Values[actionKey]));
}

ParameterInfo[] parameters = methodCall.Method.GetParameters();
for (int i = 0; i < methodCall.Arguments.Count; i++)
{
ConstantExpression param = methodCall.Arguments[i] as ConstantExpression;
string paramName = parameters[i].Name;
if (!data.Values.ContainsKey(paramName))
{
throw new RouteDataActionException(string.Format("Expected parameter '{0}', no matching parameter found", paramName));
}
else
{
string passedData = SafeString(data.Values[paramName]);
if (param.Type.IsValueType && passedData == null)
{
throw new RouteDataActionException(string.Format("Expected '{0}' parameter value of value type '{1}' , unable to convert from null", paramName, param.Type.Name));
}
object convertedData = null;
try
{
convertedData = Convert.ChangeType(passedData, param.Type);
if (!((convertedData == null && param.Value == null) || (convertedData != null && convertedData.Equals(param.Value))))
{
throw new RouteDataActionException(string.Format("Expected '{0}' parameter value of '{1}' , actual data '{2}'", paramName, param.Value, data.Values[paramName]));
}
}
catch (FormatException)
{
throw new RouteDataActionException(string.Format("Expected '{0}' parameter value of value type '{1}' , unable to convert from actual data '{2}'", paramName, param.Type.Name, data.Values[paramName]));
}
}
}

return result;

}

private static string SafeString(object value)
{
if (value == null)
{
return (string)null;
}
else
{
return value.ToString();
}
}
}
}

About Me