【问题标题】:How to mock an object that does a complicated entity framework LINQ query?如何模拟执行复杂实体框架 LINQ 查询的对象?
【发布时间】:2013-11-08 00:08:11
【问题描述】:

我正在尝试为一个简单的 mvc 控制器编写一个单元测试,该控制器对数据库进行复杂的 LINQ 查询:

public class HomeController
{
    private readonly DamagesDbContext db;

    public HomeController(DamagesDbContext db)
    {
        this.db = db;
    }

    // GET: /Home/
    [Authorize]
    public ActionResult Index()
    {
        var dashData = (from inc_c in db.incident_content
                       join inc in db.incidents
                       on inc_c.incidentid equals inc.incidentid
                       where inc.currentrevisionnumber == inc_c.revisionnumber
                       group inc_c by 1 into g
                       select new{
                           total = g.Count(),
                           open = g.Count(q => q.incidentstatus == "OPEN"),
                           closed = g.Count(q => q.incidentstatus == "CLOSED")
                       }).SingleOrDefault();

        ViewBag.total = dashData.total;
        ViewBag.open = dashData.open;
        ViewBag.closed = dashData.closed;            

        return View();
    }
}

然后在我的测试中,我有:

var mockDb = new Mock<DamagesDbContext>();
mockDb.Setup(/* What goes here? */);

var homeController = new HomeController(mockDb.Object);

var result = homeController.Index();

// Various asserts go here...

但是我在 Setup() 中应该做些什么来替换那个复杂的 LINQ 查询呢?

我如何知道调用了哪些实际方法?或者他们的论点是什么?

==== 已编辑 ====

我认为我的部分问题是 LINQ 表达式虽然有点简洁,但并没有明确说明在哪些对象上调用了哪些方法。

我第一次使用 Resharper,我只是注意到它有一个“将 LINQ 转换为方法链”选项。完成后,上面的 LINQ 表达式变为:

var dashData = (this.db.incident_content.Join(this.db.incidents, inc_c => inc_c.incidentid,
    inc => inc.incidentid, (inc_c, inc) => new {inc_c, inc})
    .Where(@t => @t.inc.currentrevisionnumber == @t.inc_c.revisionnumber)
    .GroupBy(@t => 1, @t => @t.inc_c)
    .Select(g => new
    {
        total = g.Count(),
        open = g.Count(q => q.incidentstatus == "OPEN"),
        closed = g.Count(q => q.incidentstatus == "CLOSED")
    })).SingleOrDefault();

这可能会更清楚地说明需要模拟哪些对象和方法。

【问题讨论】:

  • 附带说明:不要使用ViewBag。请改用模型。
  • 这不是生产代码,它只是一个尚未确定的功能的占位符。届时,我们将定义一个模型并将 DB 功能移动到业务层类中。但是当我们这样做时,我们仍然不知道如何模拟数据库查询。

标签: c# linq entity-framework unit-testing moq


【解决方案1】:

不要将数据库代码放在控制器中。这将是对它进行单元测试的良好开端。将您的数据库代码移动到一个单独的类,其功能是查询数据库,然后您可以模拟对该类的调用。

【讨论】:

  • 我同意我们不应该直接在控制器中进行 db 调用,并且将 db 调用移动到业务层中的类中,然后为控制器将是微不足道的。但是我需要为业务层中的类编写一个单元测试,我会回到同样的问题。
【解决方案2】:

据我所知,很多人给了你很好的建议,但没有回答这个问题。

从查询中我可以看到您需要模拟两个 DbSet(b.incident_content 和 db.incidents)并将它们添加到 FakeDbContext。然后,无论您最终将查询放在哪里,都可以使用假上下文对其进行测试。

你可以在这里伪造它:http://msdn.microsoft.com/en-us/data/dn314431.aspx

【讨论】:

    【解决方案3】:

    大型应用程序在控制器内部使用 DbContext 不是一个好习惯,但无论如何它都会出现在某个地方,并且这个地方也应该包含单元测试。所以...

    1. 最好提取IDamagesDbContext接口注入,而不是DamagesDbContext类。
    2. 您应该在公开实体的上下文属性中使用IDbSet&lt;T&gt; 接口而不是DbSet&lt;T&gt; 类。
    3. 您应该编写自己的 IDbSet&lt;T&gt; 实现,它模拟内存数据库(您可以使用 NuGet 包 FakeDbSet 已经编写的实现)。
    4. 完成上述所有操作后,您只需使用适当的数据填充内存数据库集即可实现理想的测试用例。

    【讨论】:

    • 使用 Moq 模拟 DbSet 属性似乎很简单。 FakeDbSet 会给我什么 Moq 没有?
    • 它不会给你任何不寻常的东西,只是让你免于编写像 InMemoryDbSet 这样的自定义类(或者每次都直接使用 Moq 模拟 IDbSet)。
    • 请注意,对于 EF6,您应该使用 DbSet 而不是 IDbSet
    【解决方案4】:

    请从您的控制器中解耦数据库访问逻辑
    将 Db 注入控制器

    所以现在你必须测试两个组件 一是数据库服务
    其他的是Controller服务

    注意当 public ActionResult Index() 被调用时,它会调用一些 DBService API 来检索一些结果集
    你需要包含测试来断言
    * 索引实际上是在各种场景中调用db上的RIGHT api * 使用API​​返回的结果集,并使用相同的结果集到viewBag.TOTAL, viewBagOPEN etc

    这将我们的控制器测试减少到
    1> 模拟 DBService(例如使用 Moq)
    2> 设置模拟 DBService 以期望调用 DBService.API
    3> 当从控制器调用到 mockObject 时,返回一个准备好的结果集
    4> 断言对索引的测试调用 DBService.API 否则失败
    5> 断言当调用确实发生并且返回了预先准备好的结果集时,Index 的输出使用的是相同的。

    分离的 DBService,就像您想象的那样,将有一个公共 API
    例如 public ResultSet GetAllEntitiesThatAreOpenOrClosed(这里有一些参数) 他们需要自己进行测试。
    再次了解 DBService 类的职责是什么并对其进行测试。

    【讨论】:

      【解决方案5】:

      在这种情况下我会做的是首先运行你拥有的代码,一旦你的 dashData 变量中有信息,就将它序列化到一个文件中。

      下一步,按照 Colin 的建议,将您的数据库代码拆分为一个单独的业务层对象。

      当您想要模拟业务层时,您可以使用模拟来反序列化您的文件并将捕获的输出发送到您的控制器。

      因为您确切知道 db-'result' 将是什么,您现在可以测试您的控制器是否正确响应了这组数据。

      通过这种方式,您已经隔离了控制器,并且您的单元测试将始终只测试控制器的功能。

      如果需要以非常不同的方式处理不同的数据库结果,那么您可以重复此过程以涵盖您预期的所有不同场景。

      为确保您的数据库返回正确的结果,需要在数据库端进行单元测试,这是我仍在努力解决的问题。

      最后一句话,到目前为止,我在单元测试(和 tdd)方面的经验是,我最终得到了一些对单元测试来说相当微不足道的类,而不是两个或三个大类。好消息是我在新代码上的错误率已经大幅下降,而且因为我现在可以进行回归测试,所以我很少将新错误引入旧代码中。我仍在努力掌握集成测试,但我希望能有类似的改进。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2023-03-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-12-25
        • 1970-01-01
        相关资源
        最近更新 更多