如何使用Rhino.Mocks模拟ControllerContext

时间:2010-06-20 20:21:37

标签: asp.net-mvc unit-testing tdd controller rhino-mocks

我正在尝试使用Rhino.Mocks来模拟ControllerContext对象,以便在我的控制器单元测试中访问运行时对象,如用户,请求,响应和会话。我写了下面的方法试图模拟一个控制器。

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;
 }

但是,我模拟的ControllerContext的HttpContext为空,我尝试访问HttpContext.User等导致System.NullReferenceException

我的嘲笑有什么问题?

3 个答案:

答案 0 :(得分:5)

我强烈建议您查看使用Rhino.Mocks的{​​{3}},并提供一种优雅的方法来测试您的控制器。以下是您的测试的样子:

[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();
    }
}

这是MVCContrib.TestHelperunit test,是我写的controller的一部分。

答案 1 :(得分:1)

其他答案已经展示了如何模拟物业链以解决您的问题。

但真正的问题是,如果违反law of demeter,单元测试和模拟效果不会很好。如果您希望代码可测试且最大可重用,那么您需要直接注入代码的真实依赖项并隐藏抽象背后的依赖项。

例如,而不是这样做:

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
   }
}

这样做:

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

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

通过引入INameProvider概念,您的组件代码,测试和模拟变得更加简单。您的代码也变得更加可重用:它只依赖于“名称提供者”的抽象概念,而不是依赖于一堆ASP.NET类。只要可以实现INameProvider适配器,您就可以在任何环境中重用组件。

权衡是你需要声明INameProvider接口并编写一个实现它的包装类。当您始终遵循此方法时,您将获得许多小型接口和适配器类。这就是测试驱动开发的方式。

(如果您想知道为什么我引入INameProvider而不是直接设置名称 - 这样IoC容器就可以使用该接口来匹配依赖项与实现。)

答案 2 :(得分:0)

我认为问题在于你需要存根整个属性链,或者至少传递给你的ControllerContext Mock一个HttpContext,即类似的东西:

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;
 }

在你的代码中,假设你从未将HttpContext设置为任何具体的东西,默认情况下你的Stub假设它为null。

我没有使用Darin描述的解决方案,但它看起来会让你的生活更轻松!