如何用EF6, MVC和MOQ有效地模拟单元测试的数据上下文

本文关键字:单元测试 模拟 数据 上下文 有效地 MOQ EF6 何用 MVC | 更新日期: 2023-09-27 18:13:18

我试图添加单元测试到一个新的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。我写了很多代码来尝试模拟这个查询所需的数据,主要是填充导航属性的子集合,但这需要很多时间,警钟开始响起,也许我做错了。

是否有一种更有效的方法来模拟包含许多表的复杂数据集?我只是觉得我花了几个小时试图模拟数据来测试只需要几秒钟编写的代码是错误的。

如何用EF6, MVC和MOQ有效地模拟单元测试的数据上下文

模拟数据库集总是很困难的,并且您无法简化此任务。问题是,你需要在控制器中这样做吗?答案是否定的。

Controller是一个聚合点,应该这样进行测试。单元测试控制器,以确定某些数据库查询是否工作,敲响了关注分离和单一职责原则违反的警钟。首先,我将提取数据访问层并将其隐藏在抽象层后面:

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构建。这个职责可以再次被提取出来并隐藏在抽象工厂模式后面:

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

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

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

这就是为什么最好在真实数据库上测试roleService。这篇文章在某种程度上提到了这一点:

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

因此,建议总是包含某种程度的端到端测试(除了单元测试之外),以确保应用程序在数据库上正确工作。

总结起来,我建议如下:

  • 重构控制器,以抽象任何额外的责任,它现在纳入
  • 轻松单元测试控制器和任何新类,同时模拟依赖
  • 在实际数据库上集成测试数据库服务

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

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