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

C# 如何使用EF6、MVC和MOQ有效地模拟单元测试的数据上下文,c#,asp.net-mvc,unit-testing,moq,C#,Asp.net Mvc,Unit Testing,Moq,我正在尝试将单元测试添加到新的MVC应用程序中,并遵循以下指南: 该指南几乎准确地描述了我想要完成的任务——测试控制器的Index()操作中返回的结果是否正确排序,但该示例过于做作,无法满足我的需要。在我的例子中,我的ViewModel由许多域实体组成,我发现模仿它太单调了 “我的控制器”操作中的查询如下所示: var roles = _db.Roles .OrderBy(r => r.Area.Application.Name) .The

我正在尝试将单元测试添加到新的MVC应用程序中,并遵循以下指南:

该指南几乎准确地描述了我想要完成的任务——测试控制器的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();
从这里您可以看到,我加入了众多的数据库集。我已经编写了很多代码来尝试和模拟此查询所需的数据,主要是填充导航属性的子集合,但这需要花费很多时间,而且警钟开始响起,可能我做的都是错的


有没有更有效的方法来模拟包含大量表的复杂数据集?我花了数小时试图模拟数据来测试代码,而编写代码却花费了数秒时间,这让我感觉不对。

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

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());
var角色=新角色[]
{
新角色
{
...
},
...
};
var mockRepository=new Mock();
mockRepository.Setup(r=>r.QueryRoles()).Returns(roles.AsQueryable());

模拟数据库集总是很困难的,要简化这项任务,您所能做的不多。问题是,您是否需要在控制器中执行此操作?答案是否定的

控制器是一个聚合点,因此应该对其进行测试。对控制器进行单元测试,以确定某些数据库查询是否有效,从而确定是否存在冲突。首先,我将提取数据访问层并将其隐藏在抽象后面:

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();
这会让学员暂时询问问题。让我们来看看进一步的改进--VIEW模型的构建。这一责任可以再次被抽离并隐藏在:

现在,模拟
roleService
roleViewModelFactory
应该很简单。因此,控制器的单元测试将是小而简单的(这是一件好事)。与
roleViewModelFactory
的单元测试相同——简单且独立

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

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

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

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

最后,我建议采取以下方法:

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

谢谢吉米,这里的信息很棒。我一直在努力避免抽象,因为太多的讨论导致了这个(乏味的)视频,关于为什么存储库/抽象会导致混乱、无法维护的代码:但我认为您的抽象工厂模式示例非常吸引人,所以我将再试一次!感谢Jan,许多示例添加了这个额外的层(存储库/工作单元模式或其他),我一直在尝试避免它,但我认为您是对的,它将使模拟测试控制器变得更容易。谢谢
var roles = roleService
    .GetOrderedRoles()
    .Select(role => roleViewModelFactory.CreateFromRole(role))
    .ToList();