【问题标题】:How do I use Rhino.Mocks to mock a ControllerContext如何使用 Rhino.Mocks 模拟 ControllerContext
【发布时间】:2010-06-20 20:21:36
【问题描述】:

我正在尝试使用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

我的嘲笑有什么问题?

【问题讨论】:

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


    【解决方案1】:

    我强烈建议您查看MVCContrib.TestHelper,它使用Rhino.Mocks 并提供了一种优雅的方式来测试您的控制器。您的测试可能如下所示:

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

    这是一个unit test 用于controller,它是我写的sample ASP.NET MVC application 的一部分。

    【讨论】:

    • 这很好用。仍然必须在 HttpContext.User 属性中放置一个对象,但这很容易通过以下方式完成:HttpContext.User = new GenericPrincipal(new GenericIdentity(loginName), null);
    【解决方案2】:

    其他答案已经展示了如何模拟属性链来解决您的问题。

    但这里真正的问题是,如果你违反了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 容器可以使用接口将依赖项与实现匹配。)

    【讨论】:

    • 这是一种常用的方法,但会导致 mvc 层与自定义代码的重复。即用户的 INameProvider、控制器的服务类、会话... Cookie、请求、响应和更容易抽象的是操作结果。
    【解决方案3】:

    我认为问题在于您需要对整个属性链进行存根,或者至少将 Mock HttpContext 传递给您的 ControllerContext,即类似于以下内容:

    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 假定它为空。

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

    【讨论】:

      猜你喜欢
      • 2011-09-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-04-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多