views:

99

answers:

2

I have edited and simplified this question a lot.

If I have this method on my HomeController:

    public ActionResult Strangeness( int id )
    {
        StrangenessClass strangeness = null;

        if( id == 1 )
        {
            strangeness = new StrangenessClass() { Name="Strangeness", Desc="Really weird behavior" };
        }

        return View( strangeness );
    }

And have this class:

public class StrangenessClass
{
    public string Name { get; set; }
    public string Desc { get; set; }
}

Why does this unit test fail?

    [TestMethod]
    public void Strangeness()
    {
        HomeController controller = new HomeController();

        ViewResult result = controller.Strangeness( 1 ) as ViewResult;
        var model = result.ViewData.Model;
        result = controller.Strangeness( 2 ) as ViewResult;
        model = result.ViewData.Model;

        Assert.IsNull( model );
    }

I understand that normally, I would have one test to test the null condition and another to test a good condition, but I ran into this problem while testing my delete controller. On a delete test, I would normally fetch the record, delete the record, and then attempt to fetch it again. It should be null the second time I fetch it, but it wasn't. So, I boiled the problem down as described above.

If this is not the proper way to test deletes, how would you do it? Don't you need to make sure that the record was actually deleted?

A: 

It is not clear what you are testing. In the Arrange section of your test method you are calling the first Delete action and in the Act section you are calling the second. So are you testing the controller? If yes then why are you calling the first Delete method in the Arrange section?

Also what's the _stateService variable? Is it an interface or are you actually deleting records in the database in your unit/integration test?

So I would recommend you writing multiple tests, each one verifying a precise behavior of the subject under test which I assume is the controller. So you should separate unit tests for the different Delete actions you are testing.

Assuming that _stateService is an interface that could be mocked, which is how I would recommend you design your controller, your test could look like this (using Rhino Mocks and MVCContrib.TestHelper):

[TestClass]
public class DevisControllerTests : TestControllerBuilder
{
    private HomeController _sut; // Subject Under Test
    private IStateService _stateServiceStub; // Dependency of the SUT

    [TestInitialize()]
    public void MyTestInitialize()
    {
        _stateServiceStub = MockRepository.GenerateStub<IStateService>();
        _sut = new HomeController(_stateServiceStub);
        InitializeController(_sut); // this method comes from the base class TestControllerBuilder
    }

    [TestMethod]
    public void HomeController_Delete_Action_Should_Fetch_State_From_Db_And_Pass_It_To_The_View()
    {
        // arrange
        var id = 4;
        var expectedState = new State();
        _stateServiceStub.Stub(x => x.GetById(id)).Return(expectedState);

        // act
        var actual = _sut.Delete(id);

        // assert
        actual
            .AssertViewRendered()
            .WithViewData<State>()
            .ShouldBe(expectedState);
    }

    [TestMethod]
    public void HomeController_Delete_Action_Handler_Should_Return_Default_View_If_Model_Null()
    {
        // act
        var actual = _sut.Delete(null);

        // assert
        actual.AssertViewRendered();
    }

    [TestMethod]
    public void HomeController_Delete_Action_Handler_Should_Return_View_If_Exception_Thrown_From_Service()
    {
        // arrange
        var model = new State();
        _stateServiceStub.Stub(x => x.Delete(model)).Throw(new Exception("oops"));

        // act
        var actual = _sut.Delete(state);

        // assert
        actual
            .AssertViewRendered()
            .WithViewData<State>()
            .ShouldBe(model);
    }


    [TestMethod]
    public void HomeController_Delete_Action_Handler_Should_Redirect_If_Model_Successfully_Deleted()
    {
        // arrange
        var model = new State();

        // act
        var actual = _sut.Delete(state);

        // assert
        actual
            .AssertActionRedirect()
            .ToAction<HomeController>(c => c.Index());

        _stateServiceStub.AssertWasCalled(x => x.Delete(model));
    }

}
Darin Dimitrov
There are two delete controller methods as I posted. The first to retrieve the state to be deleted, and the second post method to do the actual deletion. The _stateService variable is an instance of my state service which is based on an interface and defers everything to a FakeStateRepoditory for Unit testing. My TestInitialize sets all that up so I have consistsnt data for each test. The arrange is getting the record I want to delete, the act is trying to delete it and then fetch it again, and the asset is making sure it is null and that we were sent back to the Index method.
Brian McCord
To clarify further, I do have multiple delete tests to make sure that the first delete controller method returns the correct view data, to make sure that I am redirected to the proper place if I try to delete a record that doesn't exist, etc. This particular test it making sure that the record actually gets deleted. To do that, I must fetch it, send it to be deleted, and attempt to fetch it again to make sure it is gone.
Brian McCord
When trying to use your method, I get the following error on the HomeController_Delete_Action_Handler_Should_Redirect_If_Model_Successfully_Delete test:Could not find a parameter named 'controller' in the result's Values collection.If I change it to: actual .AssertActionRedirect() .ToAction("Index");it works. But, I prefer your method. Do you know what I'm doing wrong?
Brian McCord
@Brian, please see my answer http://stackoverflow.com/questions/2977580/error-using-mvccontrib-testhelper/2979403#2979403
Darin Dimitrov
A: 

You should not reuse a controller to handle multiple requests, which is exactly what you are doing here.

Anyway, if you check the source code for MVC you'll find the reason for this behavior:

protected internal virtual ViewResult View(string viewName, string masterName, object model)
{
    if (model != null)
    {
        base.ViewData.Model = model;
    }
    return new ViewResult { ViewName = viewName, MasterName = masterName, ViewData = base.ViewData, TempData = base.TempData };
}

If the model is null, it's not assigned to the ViewData.Model property. If you want the correct behaviour, create a new controller for your second call to HomeController.Strangeness.

Marnix van Valen