Having trouble with testing some Area routes

Dec 11, 2011 at 3:00 AM

Hi folks,

I've got the following routes .. which work fine when I run my MVC app.

/tags

/tag/{tag} .. eg. /tag/pewpew

Running those in my browser goes to the correct (area) controller/action method.

Using MvcRouteTester gives me a runtime error, which I just can't figure out how to fix.

// Arrange :P
private RouteTester<MvcApplication> RouteTester
{
    get { return new RouteTester<MvcApplication>(); }
}

[Test]
// ReSharper disable InconsistentNaming
public void GivenATagsRoute_WithIncomingRequest_ShouldMatcheTheRoute()
// ReSharper restore InconsistentNaming
{
    // Act & Assert.
    RouteTester.WithIncomingRequest("/tags").ShouldMatchRoute("Tags", "Tags", "Index", null);
}

[Test]
// ReSharper disable InconsistentNaming
public void GivenATagRouteAndTag_WithIncomingRequest_ShouldMatcheTheRoute()
// ReSharper restore InconsistentNaming
{
    // Act & Assert.
    RouteTester.WithIncomingRequest("/tag/apple").ShouldMatchRoute("Tags", "Tags", "Index", new { tag = "apple"});
}

 

And the run time error is (for the first unit test...)

MvcRouteUnitTester.AssertionException : Area name mismatch. Expected: "Tags", but was: "" (for url: "/tags").

Any suggestions what I've done wrong?

Coordinator
Dec 12, 2011 at 3:29 PM

Yep... you are instantiating the RouteTester wrong.

When you do this:

return new RouteTester<MvcApplication>();

You are getting the route table defined in your Global.asax, NOT the one defined in your Area. Remember, in your Global.asax, there are no areas defined.

So, when you assert that there should be a "Tags" area, that assertion fails. You are telling the test there should be a "Tags" area. The test is telling you there isn't one in the route table you gave it, which is absolutely correct.

From the home page documentation on testing Area routes:

"This time, you instantiate the RouteTester by passing in the class that contains your Area route definitions."

If the area is named "Tags", then that class is presumably named TagAreaRegistration. So, try this:

return new RouteTester<TagAreaRegistration>();

Let me know if that works for you.

Dec 13, 2011 at 8:25 AM

Awesome. works. Silly skim-reading on my behalf.

Cheers and love your work!

Dec 14, 2011 at 9:33 AM

Hi LDumond,

got another area route question.

I have the following unit test :-

// products/create (HttPost)
[Test]
// ReSharper disable InconsistentNaming
public void GivenAProductCreateHttpPostRoute_WithIncomingRequest_MatchesTheRoute()
// ReSharper restore InconsistentNaming
{
    // Act & Assert.
    RouteTester
        .WithIncomingRequest("/products/create""POST")
        .ShouldMatchRoute("products""products""create",
                            new {createOrEditViewModel = new _CreateOrEditViewModel()});
}

 

and that fails. Now, before I post my (very very simple) products area i was hoping you might suggest what the route should be.

Now the reason i'm asking this is because when i run my website with IIS Express .. i can goto /products/create ... enter in some product details .. and then click submit.  and the product is POST'd, saved and the redirected to details.

Secondly, i'm sure my proper area has been registered because my other two tests, both pass....

// Arrange :P
private RouteTester<ProductsAreaRegistration> RouteTester
{
    get { return new RouteTester<ProductsAreaRegistration>(); }
}
 
// products/details
[Test]
// ReSharper disable InconsistentNaming
public void GivenAProductDetailRoute_WithIncomingRequest_MatchesTheRoute()
// ReSharper restore InconsistentNaming
{
    // Act & Assert.
    RouteTester.WithIncomingRequest("/products/details")
        .ShouldMatchRoute("products""products""details");
}
 
#region Product Route Tests
 
// products/create
[Test]
// ReSharper disable InconsistentNaming
public void GivenAProductCreateRoute_WithIncomingRequest_MatchesTheRoute()
// ReSharper restore InconsistentNaming
{
    // Act & Assert.
    RouteTester
        .WithIncomingRequest("/products/create")
        .ShouldMatchRoute("products""products""create");
}

 

I'm guessing I've done something wrong because of how the POST /products/create requires a very specific ViewModel ... ??

Any suggestions?

Coordinator
Dec 14, 2011 at 1:34 PM

If you remove the "POST" like this:

RouteTester
        .WithIncomingRequest("/products/create")
        .ShouldMatchRoute("products", "products", "create",
                            new {createOrEditViewModel = new _CreateOrEditViewModel()});

 

It will pass, correct?

Dec 14, 2011 at 1:44 PM

nope. still fails. the other two pass, still.

here's my Area Registration.

using System.Web.Mvc;
 
namespace AWing.Application.Web.Areas.Products
{
    public class ProductsAreaRegistration : AreaRegistration
    {
        public override string AreaName
        {
            get { return "Products"; }
        }
 
        public override void RegisterArea(AreaRegistrationContext context)
        {
            context.MapRoute(
                "Products_default",
                "Products/{action}/{id}",
                new { controller="Products", action = "Index", id = UrlParameter.Optional }
                );
 
            //context.MapRoute(
            //    "Products_default",
            //    "Products/{controller}/{action}/{id}",
            //    new { action = "Index", id = UrlParameter.Optional }
            //);
        }
    }
}

Nothing too complex .. and of course, the action method.

[HttpPost]
public ActionResult Create(_CreateOrEditViewModel createOrEditViewModel)
{
    if (!ModelState.IsValid)
    {
        return View(createOrEditViewModel);
    }
... snip ...
 }
Coordinator
Dec 14, 2011 at 1:54 PM

So this should pass then:

RouteTester
        .WithIncomingRequest("/products/create")
        .ShouldMatchRoute("Products", "Products", "Create");
                          

Is that correct?

Dec 14, 2011 at 10:56 PM

yep! that passes . that's one of the two routes (out of the three) that does pass .. as listed above in previous post.

Coordinator
Dec 14, 2011 at 11:21 PM

Well, you have two problems here.

1) The reason this doesn't pass: 

.ShouldMatchRoute("products", "products", "create",
                            new {createOrEditViewModel = new _CreateOrEditViewModel()});

is that the object you are supposed to pass in should contain the parameter values from your URL, plus the default parameter values from your route. There is no createOrEditViewModel in either your URL or your route table defaults.

2) The reason this doesn't pass:

.WithIncomingRequest("/products/create", "POST")

is that the "POST" is there to test routes constrained using an HttpMethodConstraint, and you have no such constraint in your route table. As pointed out in the documentation, having an HttpMethodConstraint on your route definition is completely DIFFERENT than having an [HttpPost] on your actions. These route tests only tests your routes, NOT your action methods.

In other words, putting an [HttpPost] on an action method has nothing to do with the fact of whether the route will be matched or not.

Dec 14, 2011 at 11:26 PM

Ahh. doh. i totally forgot that attributes are ignored. OK. so that's fine...

but what about the first part -> custom view models. You're saying that I need to explicitly define all the properties that are in my viewModel .. in the route?

Coordinator
Dec 15, 2011 at 12:16 AM
Edited Dec 15, 2011 at 12:18 AM

No, not at all. But again, I think you are mixing up testing routes with testing actions.

Remember that in these tests, you are only testing what routes are MATCHED by a given URL. The library knows nothing of what happens in the request pipeline after the route is actually matched, which means it knows nothing about your view models. This is exactly as it should be, since your viewModel has nothing to do with how your application matches a URL to a route.

When the application starts going down your route list to see where the first match is, it only uses a few pieces of information to determine that match. First, it considers the parameters embedded in your actual URL string (including the controller, the action, and any additional parameters after that). Then, it considers any default values defined in the route (except UrlParameter.Optional, which is ignored). Then, in the case of area routes, it looks at the values in the DataTokens array. That's it.

Anything having to do with passing a viewModel to an action method comes well AFTER the route match is made. If you want to test that, then you have to write a different test -- one that actually instantiates your controller, then invokes the action, passing a fake viewModel to the action as a parameter, and asserting against the action result in the usual way.

Make sense?

Dec 15, 2011 at 12:52 AM

Yep. Sure does.

What I'm getting confused about is this part :-

>>When the application starts going down your route list to see where the first match is, it only uses a few pieces of information to determine that match. First, it considers the parameters embedded in your actual URL string (including the controller, the action, and any additional parameters after that). 

I thought that .. for an ActionMethod that accepts a complex type (such as my ViewModel) then that viewmodel needs to be part of the URL string .. somehow.

That's what confused me so much. When i removed the viewmodel from the .ShouldMatchRoute(..) and added in an HttpMethodConstraint .. it worked.

So .. i know I'm trying to test the ROUTE .. not the controller. But to generate a route .. if it has a complex type .. that complex type is NOT part of the route, is it? So if I have a classic CREATE (GET) page .. and that lists all the form data .. that form data is not part of the route. So even though my ViewModel is a parameter for an ActionMethod .. that doesn't mean it was part of the URL that is directly related to that ActionMethod! 

GOTCHA!

was so confused.

phew :)

Take - Away from all of this: ActionMethod arguments do not reflect the route required to call that ActionMethod. Form items != Url items :)

Coordinator
Dec 15, 2011 at 1:10 AM

By George, I think you've got it! :)

You are correct -- the form data / viewModel does not participate in route matching at all.