views:

610

answers:

2

I'm pretty new to testing and mocking and i am trying to write a test that will ensure that my validation logic is setting ModelState errors correctly.

What I'm seeing is that the controller.ControllerContext.HttpContext.Request is set the first time I check it but every time after that the Request is null.

This is causing a null reference exception in the PopulateDictionary method of the *ValueProviderDictionary * class in the MVC source because the request object is accessed multiple times in that method without ensuring the request is not null.

I'm gluing together several techniques and helpers that I have found while researching how to overcome some of the issues I have run into thus far so at this point I'm a little unsure where I may have introduced the issue.

Am I using mock objects incorrectly here?

Failing Test

//Test
public void Test_FooController_OnActionExecuting_ShouldMapStateToAFooModel()
{
    //Arrange
    DataAccessFactoryMocks.MockAllDaos();

    var controller = new FooController();

    var testFormCollection = new NameValueCollection();
    testFormCollection.Add("foo.CustomerID", "3");
    testFormCollection.Add("_fooForm", SerializationUtils.Serialize(new FooModel()));

    var mockHttpContext = new MockHttpContext(controller, "POST", testFormCollection, null);

    //Accessor used to run the protected OnActionExecuting method in my controller
    var accessor = new FooControllerAccessor(controller);

    //Request is set, assertion passes
    Assert.IsNotNull(controller.ControllerContext.HttpContext.Request.Form);

    //Request is null when accessing the property a second time, assertion fails
    Assert.IsNotNull(controller.ControllerContext.HttpContext.Request.QueryString);

    //Act
    accessor.OnActionExecuting(new ActionExecutingContext(controller.ControllerContext, MockRepository.GenerateStub<ActionDescriptor>(), new Dictionary<string, object>()));

    //Assert
    Assert.That(controller.ModelState.IsValid == false);
}

Test Helper

//Test helper to create httpcontext and set controller context accordingly
public class MockHttpContext
{
    public HttpContextBase HttpContext { get; private set; }
    public HttpRequestBase Request { get; private set; }
    public HttpResponseBase Response { get; private set; }
    public RouteData RouteData { get; private set; }

    public MockHttpContext(Controller onController)
    {
        //Setup the common context components and their relationships
        HttpContext = MockRepository.GenerateMock<HttpContextBase>();
        Request = MockRepository.GenerateMock<HttpRequestBase>();
        Response = MockRepository.GenerateMock<HttpResponseBase>();

        //Setup the context, request, response relationship
        HttpContext.Stub(c => c.Request).Return(Request);
        HttpContext.Stub(c => c.Response).Return(Response);

        Request.Stub(r => r.Cookies).Return(new HttpCookieCollection());
        Response.Stub(r => r.Cookies).Return(new HttpCookieCollection());

        Request.Stub(r => r.QueryString).Return(new NameValueCollection());
        Request.Stub(r => r.Form).Return(new NameValueCollection());

        //Apply the context to the suppplied controller
        var rc = new RequestContext(HttpContext, new RouteData());
        onController.ControllerContext = new ControllerContext(rc, onController);
    }

    public MockHttpContext(Controller onController, string httpRequestType, NameValueCollection form, NameValueCollection querystring)
    {
    //Setup the common context components and their relationships
    HttpContext = MockRepository.GenerateMock<HttpContextBase>();
    Request = MockRepository.GenerateMock<HttpRequestBase>();
    Response = MockRepository.GenerateMock<HttpResponseBase>();

    //Setup request type based on parameter value
    Request.Stub(r => r.RequestType).Return(httpRequestType);

    //Setup the context, request, response relationship
    HttpContext.Stub(c => c.Request).Return(Request);
    HttpContext.Stub(c => c.Response).Return(Response);

    Request.Stub(r => r.Cookies).Return(new HttpCookieCollection());
    Response.Stub(r => r.Cookies).Return(new HttpCookieCollection());

    Request.Stub(r => r.QueryString).Return(querystring);
    Request.Stub(r => r.Form).Return(form);

    //Apply the context to the suppplied controller
    var rc = new RequestContext(HttpContext, new RouteData());
    onController.ControllerContext = new ControllerContext(rc, onController);
    }
}

Working Test using MvcContrib.TestHelper

    public void Test_FooController_OnActionExecuting_ShouldMapStateToAFooModel()
    {
        //Arrange
        DataAccessFactoryMocks.MockAllDaos();

        TestControllerBuilder builder = new TestControllerBuilder();

        builder.Form.Add("fooModel.CustomerID", "3");

        builder.HttpContext.Request.Stub(r => r.RequestType).Return("POST");

        FooController controller = builder.CreateController<FooController>();

        var accessor = new FooControllerAccessor(controller);

        //Act
        accessor.OnActionExecuting(new ActionExecutingContext(controller.ControllerContext, MockRepository.GenerateStub<ActionDescriptor>(), new Dictionary<string, object>()));

        //Assert
        Assert.IsFalse(controller.ModelState.IsValid);
    }
+4  A: 

I would suggest you using the excellent MVCContrib TestHelper for unit testing your ASP.NET MVC controllers with Rhino Mocks. You will see a drastic simplification of your unit tests and increased readability.

Darin Dimitrov
Thanks, I will look into them since I would eventually like to eliminate all of this setup for each test. As it is though i still don't understand why the property is being cleared and I would like to write a few long winded tests to help me understand what's happening. The TestHelper seems to hide a lot of the inner workings that I think are beneficial to understand.
jjr2527
Because when you are defining the expectation you need to specify .Repeat.Any()
Darin Dimitrov
I got it to work using the TestHelper. .Repeat.Any() still didn't fix the original test so I must be doing something wrong still. I'm happy with what came out. Code updated above.
jjr2527
Man! I love you :)
HeavyWave
A: 

I am seeing the exact same thing with MSpec, using StructureMap and the MvcFakes project...

Below is as example controller test, which should return the following JSON string (generated from business rules validation in the domain layer, then used to display an informative message via custom JQuery plugin):

[{"Key":"Name","Value":"Name field is incomplete"}]

The first class in this testing namespace runs OK...

Product creation, when creating a product without specifiying its Name

  • should display an informative error to the user

...however any further tests in the same namespace fail.

Having tried a few combinations of testing frameworks, I can only get the default VS testing framework to run these tests, but this is not ideal.

Does anyone have any hints?

[Subject(typeof(Product), "creation")]
public class when_creating_a_product_without_specifiying_its_Name
{
    private static AddProductController target;
    private static string result;
    private static NameValueCollection formParams;
    private static NameValueCollection queryStringParams;

    Establish context = () =>
    {
        Bootstrapper.ConfigureStructureMap();
        target = ObjectFactory.GetInstance<AddProductController>();

        queryStringParams = new NameValueCollection { 
                    { "CategoryId", "1" }, 
                    { "SubCategoryId", "44" } 
                };
        formParams = new NameValueCollection { 
                    { "Product.Value", "100" },
                    { "Category", "testcat" },
                    { "SubCategory", "testsubcat" }
                };
        target.ControllerContext = new FakeControllerContext(target, "testUser", null, formParams, queryStringParams, null, null);
    };
    Because of = () =>
    {
        result = target.SubmitForm();
    };
    It should_display_an_informative_error_to_the_user = () =>
    {
        result.ShouldContain("Name field is incomplete");
    };

}
Rob Bowall