【问题标题】:How to efficiently mock Data Contexts for Unit Testing with EF6, MVC and MOQ如何使用 EF6、MVC 和 MOQ 有效地模拟数据上下文以进行单元测试
【发布时间】:2014-07-16 07:04:16
【问题描述】:

我正在尝试将单元测试添加到新的 MVC 应用程序中,并且我正在遵循以下指南: http://msdn.microsoft.com/en-us/data/dn314429

该指南几乎准确地详细说明了我想要完成的任务 - 测试控制器的 Index() 操作中返回的结果是否正确排序,但该示例对于我的需要来说太做作了。在我的例子中,我的 ViewModel 由许多领域实体组成,我发现模拟它过于繁琐。

我的控制器动作中的查询如下:

var roles = _db.Roles
            .OrderBy(r => r.Area.Application.Name)
            .ThenBy(r => r.Area.Name)
            .ThenBy(r => r.Name)
            .Select(role =>
                new RoleViewModel
                {
                    RoleName = role.Name,
                    Description = role.Description,
                    ApplicationArea = role.Area.Application.Name + "/" + role.Area.Name,
                    GroupsUsingThisRole = role.RoleGroupMappings
                        .Select(rgm => rgm.Group.Name).ToList()
                }).ToList();

从这里你可以看到我加入了许多 DBSet。我编写了很多代码来尝试模拟此查询所需的数据,主要是填充导航属性的子集合,但这需要很多时间,而且警钟开始响起,也许我做错了。

是否有更有效的方法来模拟包含大量表的复杂数据集?我花费数小时试图模拟数据来测试需要几秒钟才能编写的代码,这感觉不对。

【问题讨论】:

    标签: c# asp.net-mvc unit-testing moq


    【解决方案1】:

    Mocking DB 集总是很困难,您无法做太多事情来简化这项任务。问题是,您需要在控制器中执行此操作吗?答案是——不。

    Controller 是一个聚合点,因此应该对其进行测试。用于确定某些数据库查询是否有效的单元测试控制器敲响了separation of concernssingle responsibility principles 违规的警钟。首先,我会提取数据访问层并将其隐藏在抽象之后:

    var roles = roleService
        .GetOrderedRoles()
        .Select(role =>
                new RoleViewModel
                {
                    RoleName = role.Name,
                    Description = role.Description,
                    ApplicationArea = role.Area.Application.Name
                        + "/" + role.Area.Name,
                    GroupsUsingThisRole = role.RoleGroupMappings
                        .Select(rgm => rgm.Group.Name).ToList()
                })
        .ToList();
    

    这暂时解决了查询问题。让我们来看看进一步的改进——ViewModel 的构建。这个责任可以再次被抽离并隐藏在abstract factory pattern后面:

    var roles = roleService
        .GetOrderedRoles()
        .Select(role => roleViewModelFactory.CreateFromRole(role))
        .ToList();
    

    现在,模拟 roleServiceroleViewModelFactory 应该是微不足道的。结果,控制器的单元测试将变得小而简单(这是一件好事)。与roleViewModelFactory 的单元测试相同——简单且隔离。

    最后,我们需要解决最初的问题——单元测试数据库层。但是我们吗?对数据库进行单元测试?我们可以检查服务是否在数据库上下文中调用了适当的方法,但这又是很多设置工作。更糟糕的是,如果我们隔离(模拟)数据库层,我们实际上隔离了我们服务的单一职责——与数据库对话

    这就是为什么最好在真实数据库上测试roleService。有问题的文章以某种方式提到了这一点:

    内存中测试替身可能是为使用 EF 的应用程序的位提供单元测试级别覆盖的好方法。但是,在执行此操作时,您将使用 LINQ to Objects 对内存中的数据执行查询。这可能会导致与使用 EF 的 LINQ 提供程序 (LINQ to Entities) 将查询转换为针对您的数据库运行的 SQL 不同的行为。 (…)

    因此,建议始终包含某种级别的端到端测试(除了您的单元测试),以确保您的应用程序针对数据库正常工作。

    总结一下,我建议以下方法:

    • 重构控制器以抽象它现在包含的任何其他职责
    • 在模拟依赖项时轻松对控制器和任何新类进行单元测试
    • 在实际数据库上集成测试数据库服务

    【讨论】:

    • 谢谢吉米,这里有很棒的信息。我一直在努力避免抽象,因为太多的讨论导致了这个(乏味的)视频,关于为什么存储库/抽象会导致混乱、无法维护的代码:youtube.com/watch?v=0tlMTJDKiug 但我认为你的抽象工厂模式示例真的很性感,所以我会给出再试一次!
    【解决方案2】:

    好吧,你可以在你的控制器和数据库之间插入一些层,一些存储库。然后您可以模拟存储库以返回模拟数据。像这样的:

    public interface IRoleRepository
    {
       IQueriable<Role> QueryRoles();
    }
    

    然后在您的测试中,您只创建一组模拟角色并在模拟存储库中返回:

    var roles = new Role[]
    {
       new Role
       {
          ...
       },
       ...
    };
    
    var mockRepository = new Mock<IRoleRepository>();
    mockRepository.Setup(r => r.QueryRoles()).Returns(roles.AsQueryable());
    

    【讨论】:

    • 谢谢 Jan,许多示例都添加了这个额外的层(存储库/工作单元模式或其他),我一直在努力避免它,但我认为你是对的,它会使模拟更容易测试控制器。谢谢!
    猜你喜欢
    • 1970-01-01
    • 2023-03-29
    • 1970-01-01
    • 2013-07-08
    • 1970-01-01
    • 2022-10-13
    • 2021-08-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多