views:

399

answers:

4

I have a controller that implements a simple Add operation on an entity and redirects to the Details page:

[HttpPost]
public ActionResult Add(Thing thing)
{ 
    // ... do validation, db stuff ...
    return this.RedirectToAction<c => c.Details(thing.Id));
}

This works great (using the RedirectToAction from the MvcContrib assembly).

When I'm unit testing this method I want to access the ViewData that is returned from the Details action (so I can get the newly inserted thing's primary key and prove it is now in the database).

The test has:

var result = controller.Add(thing);

But result here is of type: System.Web.Mvc.RedirectToRouteResult (which is a System.Web.Mvc.ActionResult). It doesn't hasn't yet executed the Details method.

I've tried calling ExecuteResult on the returned object passing in a mocked up ControllerContext but the framework wasn't happy with the lack of detail in the mocked object.

I could try filling in the details, etc, etc but then my test code is way longer than the code I'm testing and I feel I need unit tests for the unit tests!

Am I missing something in the testing philosophy? How do I test this action when I can't get at its returned state?

+2  A: 

You seem to be doing way too much for a unit test. The validation and data access would typically be done by services that you call from the controller action. You mock those services and only test that they were called properly.

Something like this (using approximate syntax for Rhino.Mocks & NUnit):

[Test]
public void Add_SavesThingToDB()
{
    var dbMock = MockRepository.GenerateMock<DBService>();
    dbMock.Expect(x => x.Save(thing)).Repeat.Once();

    var controller = new MyController(dbMock);
    controller.Add(new Thing());

    dbMock.VerifyAllExpectations();
}

[Test]
public void Add_RedirectsAfterSave()
{
    var dbMock = MockRepository.GenerateMock<DBService>();

    var controller = new MyController(dbMock);
    var result = (RedirectToRouteResult)controller.Add(new Thing());

    Assert.That(result.Url, Is.EqualTo("/mynew/url"));
}
rmacfie
Thanks -- that definitely helps avoid the difficulties with the framework during testing. So, following this idiom I would have unit tests for the DBService which proves I can add things, and a unit test for the controller proving that its calls Save on the service. But I've not really proven that the thing passed into the controller ends up in the database. Maybe I could do that with a whole bunch more complex mocking rules ... but that doesn't feel right, it is a lot of test boiler plate for a simple operation.
Rob Walker
Well, determining the scope and effort to put into tests and what to test, etc, is something I struggle with too. I think you should try to separate your tests into two categories: unit tests and integration tests. The unit tests should test only very small units of functionality, such as the tests above. The integration tests should look at how everything integrates, maybe covering a small user story that you have. I would prefer to make the integration tests as close as possible to "real" usage, for example running WatiN and actually clicking a couple of links. No need to mock there at all.
rmacfie
If you test your DBService, that you prove that it works. Then, you should assume that it can and it will handle the database calls correctly. So if your controller uses this service, you know that it will be working. With the mock framework, you can validate the paramaters that are passed to the service methods and this is enough. I think rmacfie is right, you are maybe trying to test too much deeper. Your unit test should cover only one action, not a whole process.
couellet
+3  A: 

There is MVC Contrib TestHelper that are fantastic for testing most of the ActionResult

You can get it here: http://mvccontrib.codeplex.com/wikipage?title=TestHelper

Here is an example of the syntax:

var controller = new TestController();

controller.Add(thing)
          .AssertActionRedirect()
          .ToAction<TestController>(x => x.Index());

To test if the data has been persisted successfully, you should maybe ask your database directly, I don't know if you're using an ORM or something, but you should do something to get the last insterted item in your database, then compare with the value you provided to your Add ActionResult and see if this is ok.

I don't think that testing your Details ActionResult to see if your data is persisted is the right approach. That would not be an unit test, more a functional test.

But you should also unit test your Details method to make sure that your viewdata object is filled with the right data coming from your database.

couellet
+3  A: 

I am using MVC2 RC2 at the moment and the answer from rmacfie didn't quite work for me but did get me on the right track.

Rightly or wrongly I managed to do this in my test instead:

var actionResult = (RedirectToRouteResult)logonController.ForgotUsername(model);

actionResult.RouteValues["action"].should_be_equal_to("Index");
actionResult.RouteValues["controller"].should_be_equal_to("Logon");

Not sure if this will help someone but might save you 10 minutes.

ArtificialGold
A: 

I have a static helper method that tests redirection.

public static class UnitTestHelpers
{
    public static void ShouldEqual<T>(this T actualValue, T expectedValue)
        {
            Assert.AreEqual(expectedValue, actualValue);
        }

        public static void ShouldBeRedirectionTo(
                                      this ActionResult actionResult, 
                                      object expectedRouteValues)
        {
            RouteValueDictionary actualValues = 
                            ((RedirectToRouteResult)actionResult).RouteValues;
            var expectedValues = new RouteValueDictionary(expectedRouteValues);

            foreach (string key in expectedValues.Keys)
            {
                Assert.AreEqual(expectedValues[key], actualValues[key]);
            }
        }
    }
}

Then creating a redirection test is very easy.

[Test]
public void ResirectionTest()
{
    var result = controller.Action();

    result.ShouldBeRedirectionTo(
                new
                    {
                        controller = "ControllerName",
                        action = "Index"
                    });
}
Vadim