How do I use Rhino.Mocks to mock a ControllerConte

2019-02-14 19:20发布

问题:

I am trying to use Rhino.Mocks to mock up a ControllerContext object to gain access to runtime objects like User, Request, Response, and Session in my controller unit tests. I've written the below method in an attempt to mock up a controller.

private TestController CreateTestControllerAs(string userName)
{
    var mock = MockRepository.GenerateStub<ControllerContext>();
    mock.Stub(con =>
        con.HttpContext.User.Identity.Name).Return(userName);
    mock.Stub(con =>
        con.HttpContext.Request.IsAuthenticated).Return(true);

    var controller = CreateTestController(); // left out of example for brevity
    controller.ControllerContext = mock;

    return controller;
 }

However, the HttpContext of my mocked ControllerContext is null and there my attempts to access HttpContext.User etc. cause a System.NullReferenceException.

What am I doing wrong with my mocking?

回答1:

I would strongly recommend you looking at MVCContrib.TestHelper which uses Rhino.Mocks and provides an elegant way to test your controllers. Here's how your test might look like:

[TestClass]
public class UsersControllerTests : TestControllerBuilder
{
    [TestMethod]
    public void UsersController_Index()
    {
        // arrange
        // TODO : this initialization part should be externalized
        // so that it can be reused by other tests
        var sut = new HomeController();
        this.InitializeController(sut);
        // At this point sut.Request, sut.Response, sut.Session, ... are
        // stubed objects on which you could define expectations.

        // act
        var actual = sut.Index();

        // assert
        actual.AssertViewRendered();
    }
}

And here's an unit test for a controller that is part of a sample ASP.NET MVC application I wrote.



回答2:

The other answers have already shown how you can mock a property chain to work around your problem.

But the real problem here is that unit testing and mocking don't really work well if you violate the law of demeter. If you want your code to be testable and maximally reusable, then you need to directly inject the real dependencies of your code and hide those dependencies behind abstractions.

For example, instead of doing this:

public class MyClass
{
   public ControllerContext Context { get; set; }

   public void DoSomething()
   {
       // BAD: we're only interested in the name, but instead we've injected 
       // a ControllerContext that can give us a HttpContext that can give us
       // a User that can give us an Identity that can give us the Name.
       string name = Context.HttpContext.User.Identity.Name;
       // etcetera
   }
}

Do this:

public class MyClass
{
    public INameProvider NameProvider { get; set; }

    public void DoSomething()
    {
        // GOOD: we've injected a name provider
        string name = NameProvider.Name;
        // etcetera
    }
}

By introducing the INameProvider concept, your component code, tests and mocks become much simpler. Your code also becomes more reusable: it only has a dependency on the abstract concept of a "name provider", rather than on a bunch of ASP.NET classes. You will be able to reuse your component in any environment as long as it is possible to implement a INameProvider adapter.

The trade-off is that you will need to declare the INameProvider interface and write a wrapper class which implements it. When you consistently follow this approach, you will end up with a lot of small interfaces and adapter classes. Such is the way of test driven development.

(In case you're wondering why I introduce INameProvider instead of setting the name directly - this is so that the IoC container can use the interface to match up the dependency with the implementation.)



回答3:

I believe the problem is that you need to stub the whole chain of properties, or at least pass to your ControllerContext Mock a HttpContext, i.e. something along the lines of:

private TestController CreateTestControllerAs(string userName)
{
    var mock = MockRepository.GenerateStub<ControllerContext>();
    var context = MockRepository.GenerateStub<IHttpContext>();    
    mock.Stub(con =>
        con.HttpContext).Return(context );
    // etc... with User, Identity ...

    return controller;
 }

In your code, given that you never set the HttpContext to anything specifically, by default your Stub assumes it is null.

I haven't used the solution Darin describes, but it looks like it would make your life much easier!