views:

1036

answers:

3

I'm trying to write a test for an UrlHelper extensionmethod that is used like this:

Url.Action<TestController>(x => x.TestAction());

However, I can't seem set it up correctly so that I can create a new UrlHelper and then assert that the returned url was the expected one. This is what I've got but I'm open to anything that does not involve mocking as well. ;O)

     [Test]
 public void Should_return_Test_slash_TestAction()
 {
  // Arrange
  RouteTable.Routes.Add("TestRoute", new Route("{controller}/{action}", new MvcRouteHandler()));
  var mocks = new MockRepository();
  var context = mocks.FakeHttpContext(); // the extension from hanselman
  var helper = new UrlHelper(new RequestContext(context, new RouteData()), RouteTable.Routes);

  // Act
  var result = helper.Action<TestController>(x => x.TestAction());

  // Assert
  Assert.That(result, Is.EqualTo("Test/TestAction"));
 }

I tried changing it to urlHelper.Action("Test", "TestAction") but it will fail anyway so I know it is not my extensionmethod that is not working. NUnit returns:

NUnit.Framework.AssertionException: Expected string length 15 but was 0. Strings differ at index 0.
Expected: "Test/TestAction"
But was:  <string.Empty>

I have verified that the route is registered and working and I am using Hanselmans extension for creating a fake HttpContext. Here's what my UrlHelper extentionmethod look like:

     public static string Action<TController>(this UrlHelper urlHelper, Expression<Func<TController, object>> actionExpression) where TController : Controller
 {
  var controllerName = typeof(TController).GetControllerName();
  var actionName = actionExpression.GetActionName();

  return urlHelper.Action(actionName, controllerName);
 }

 public static string GetControllerName(this Type controllerType)
 {
  return controllerType.Name.Replace("Controller", string.Empty);
 }

 public static string GetActionName(this LambdaExpression actionExpression)
 {
  return ((MethodCallExpression)actionExpression.Body).Method.Name;
 }

Any ideas on what I am missing to get it working??? / Kristoffer

+1  A: 

I know this doesn't directly answer your question, but is there a reason you're trying to write your own generic extension method as opposed to using the one that is available in the MVC Futures assembly? (Microsoft.Web.Mvc.dll) Or are you in fact trying to unit test msft's extension method?

[Edit 1] Sorry, I was thinking of the Html helper extension in Futures.

In the meantime, I'll try my hand at a unit test to see if I get the same result.

[Edit 2] Ok, so this isn't completely working yet, but it's not blowing up. The result is simply returning an empty string. I took some Mvc mocking helpers from Scott Hanselman at this link.

I also created a Url.Action<TController> method, along with helper methods I ripped from the Mvc source:

public static string Action<TController>(this UrlHelper helper, Expression<Action<TController>> action) where TController : Controller
{
    string result = BuildUrlFromExpression<TController>(helper.RequestContext, helper.RouteCollection, action);
    return result;
}

public static string BuildUrlFromExpression<TController>(RequestContext context, RouteCollection routeCollection, Expression<Action<TController>> action) where TController : Controller
{
    RouteValueDictionary routeValuesFromExpression = GetRouteValuesFromExpression<TController>(action);
    VirtualPathData virtualPath = routeCollection.GetVirtualPath(context, routeValuesFromExpression);
    if (virtualPath != null)
    {
        return virtualPath.VirtualPath;
    }
    return null;
}

public static RouteValueDictionary GetRouteValuesFromExpression<TController>(Expression<Action<TController>> action) where TController : Controller
{
    if (action == null)
    {
        throw new ArgumentNullException("action");
    }
    MethodCallExpression body = action.Body as MethodCallExpression;
    if (body == null)
    {
        throw new ArgumentException("MvcResources.ExpressionHelper_MustBeMethodCall", "action");
    }
    string name = typeof(TController).Name;
    if (!name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase))
    {
        throw new ArgumentException("MvcResources.ExpressionHelper_TargetMustEndInController", "action");
    }
    name = name.Substring(0, name.Length - "Controller".Length);
    if (name.Length == 0)
    {
        throw new ArgumentException("MvcResources.ExpressionHelper_CannotRouteToController", "action");
    }
    RouteValueDictionary rvd = new RouteValueDictionary();
    rvd.Add("Controller", name);
    rvd.Add("Action", body.Method.Name);
    AddParameterValuesFromExpressionToDictionary(rvd, body);
    return rvd;
}

private static void AddParameterValuesFromExpressionToDictionary(RouteValueDictionary rvd, MethodCallExpression call)
{
    ParameterInfo[] parameters = call.Method.GetParameters();
    if (parameters.Length > 0)
    {
        for (int i = 0; i < parameters.Length; i++)
        {
            Expression expression = call.Arguments[i];
            object obj2 = null;
            ConstantExpression expression2 = expression as ConstantExpression;
            if (expression2 != null)
            {
                obj2 = expression2.Value;
            }
            else
            {
                Expression<Func<object>> expression3 = Expression.Lambda<Func<object>>(Expression.Convert(expression, typeof(object)), new ParameterExpression[0]);
                obj2 = expression3.Compile()();
            }
            rvd.Add(parameters[i].Name, obj2);
        }
    }
}

And finally, here's the test I'm running:

    [Test]
    public void GenericActionLinkHelperTest()
    {
        RouteRegistrar.RegisterRoutesTo(RouteTable.Routes);

        var mocks = new MockRepository();
        var context = mocks.FakeHttpContext(); // the extension from hanselman

        var helper = new UrlHelper(new RequestContext(context, new RouteData()), RouteTable.Routes);
        string result = helper.Action<ProjectsController>(x => x.Index());

        // currently outputs an empty string, so something is fudded up.
        Console.WriteLine(result);
    }

Not sure yet why the output is an empty string, but I'll keep messing with this as I have time. I'd be curious to know if you find a solution in the meantime.

mannish
I've updated my examples and now I get the same result as you and I guess you could call that progress. Still not sure why I get an empty string back as I have verified that my route is working and matches "~/Test/TestAction".
Kristoffer Ahl
Yeah, I'm not sure why that's coming back empty either. At first I thought it was because in the project I ran this test, I have some odd routes and perhaps it couldn't find a match. Since you're getting the same result, I'm not so sure it's a routing issue. I'll play around some more.
mannish
+1  A: 

I was able to test the BuildUrlFromExpression method, but I needed to register my RouteTable.Routes before running the tests:

[ClassInitialize]
public static void FixtureSetUp(TestContext @__testContext)
{
    MvcApplication.RegisterRoutes(RouteTable.Routes);
}

Then stub out / setup these properties:

HttpRequestBase request = mocks.PartialMock<HttpRequestBase>();
request.Stub(r => r.ApplicationPath).Return(string.Empty);

HttpResponseBase response = mocks.PartialMock<HttpResponseBase>();
SetupResult.For(response.ApplyAppPathModifier(Arg<String>.Is.Anything)).IgnoreArguments().Do((Func<string, string>)((arg) => { return arg; }));

After that the BuildUrlFromExpression method returned uls as expected.

Adam Kahtava
+2  A: 

The reason it isn't working is that internally the RouteCollection object calls the ApplyAppPathModifier method on HttpResponseBase. It looks like Hanselman's mock code does not set any expectations on that method so it returns null, which is why all of your calls to the Action method on UrlHelper are returning an empty string. The fix would be to setup an expectation on the ApplyAppPathModifier method of the HttpResponseBase mock to just return the value that is passed into it. I'm not a Rhino Mocks expert so I'm not completely sure on the syntax. If you are using Moq, then it would look like this:

httpResponse.Setup(r => r.ApplyAppPathModifier(It.IsAny<string>()))
    .Returns((string s) => s);

Or, if you just use a hand-rolled mock, something like this would work:

internal class FakeHttpContext : HttpContextBase
{
    private HttpRequestBase _request;
    private HttpResponseBase _response;

    public FakeHttpContext()
    {
        _request = new FakeHttpRequest();
        _response = new FakeHttpResponse();
    }

    public override HttpRequestBase Request
    {
        get { return _request; }
    }

    public override HttpResponseBase Response
    {
        get { return _response; }
    }
}

internal class FakeHttpResponse : HttpResponseBase
{
    public override string ApplyAppPathModifier(string virtualPath)
    {
        return virtualPath;
    }
}

internal class FakeHttpRequest : HttpRequestBase
{
    private NameValueCollection _serverVariables = new NameValueCollection();

    public override string ApplicationPath
    {
        get { return "/"; }
    }

    public override NameValueCollection ServerVariables
    {
        get { return _serverVariables; }
    }
}

The above code should be the minimum necessary implementation of HttpContextBase in order to make a unit test pass for the UrlHelper. I tried it out and it worked. Hope this helps.

Andy