Unit/integration 测试 nHibernate 查询

Unit/integration testing nHibenrate query

场景:我需要编写一个复杂的 nHibernate 查询,这将 return 投影 DTO,但我想使用 TDD 方法。该方法如下所示:

public PrintDTO GetUsersForPrinting(int userId)
{
     Session.QueryOver<User>().//some joins, conditions etc.
     //returns projected dto
}

问题:

  1. 由于最常见的方法是使用内存数据库进行此类操作。我应该写集成测试吗?
  2. 如果我在内存中使用数据库,我可以编写单元测试吗?
  3. 一次测试就够了吗?
  4. 由于我的集成测试可能会检查投影,我应该如何命名? "GetUserForPrinting_return_correct_DTO" 似乎太抽象和愚蠢了。

我问是因为:

这不是学习 TDD 的好问题。我假设您还不知道复杂的查询是什么样的,并且您想使用测试驱动技术来解决它。太棒了:)

但让我们看看我能否回答您的问题。

  1. 任何包含真实数据库的测试,无论是在内存中还是在磁盘上,都不是单元测试。单元测试将使用模拟数据库。

  2. 也许 - 如果您的查询足够复杂,那么不会。

  3. testGetUsersForPrinting 或 getUsersForPrintingTest 或类似的

我很可能会在 SQL 解释器而不是代码中排除查询。目的是根据我在此过程中学到的知识,针对内存中的数据库生成一系列集成测试。 从您能想到的最小 DTO 开始,并以此为基础进行构建。

最后将查询转换为 nhibernate 调用,然后使集成测试通过。

测试驱动,但不是真正的单元测试驱动。

如果您愿意接受最大的 TDD 纪律并处理比平常更慢和更烦人的工作,您可以在开发时自动化每个集成测试并编写代码使其通过。这将意味着您经常在 3 个抽象级别/编辑器/环境(直接 SQL 查询、集成测试、c# 代码)之间切换 - 我通过设置技术来处理这个问题,以迫使自己每次都遵循正确的步骤.

最后一点就是为什么这不是学习 TDD 的好问题。您将需要很多纪律,您可能还没有强迫自己获得这些纪律!

祝你好运。


好的一些具体例子。我会将您的代码示例修改为如下所示

public PrintDTO GetUsersForPrinting(int userId, ISession session)
{
     var data = session.QueryOver<User>().//some joins, conditions etc.

     return data; // or whatever
}

在你的单元测试中你会写

public testDTO()
{
    //Arrange
    StubSession session = .... setup a stub session, which returns hardcoded values

    // Act
    PrintDTO users = GetUsersForPrinting(111, session);

    // Assert
    Assert.That(users.size(), Is.EqualTo(1));    
    Assert.That(users.get(0).userId, Is.EqualTo(111));

}

在您的集成测试中,您将使用一个真实的数据库,您的会话对象将实际连接到它,查询将针对该数据库进行解析

Arrange-Act-Assert 是组织单元测试的标准方法。 通常,您希望在单元测试中使用尽可能少的断言。你将有多个单元测试。 当你在写单元测试的时候,先写Assert,然后把剩下的填进去,让它compile/get成为你想要的结果。让测试先失败,因为当它通过时你就知道你真的交付了一些东西。

在此示例中,要实现存根 ISession,您将从 ISession 派生一个本地 StubSession class(仅对测试套件可见),只需填写绝对最小值即可编译,并且 return 使测试通过的最少数据。


要构建您的整个 DTO - 假设您知道您在 DTO 中想要什么 - 正如您在评论中所说,逐步进行。构建 DTO 的每个部分 一次一块,每块加一个单元测试。

跟踪这是 TDD 的另一部分。

为自己设置一个 TODO 列表 - 只是一个简单的文本文件,或者可能是测试套件开头的冗长注释。列出您要测试的所有内容,例如零个结果,一个结果,两个结果,20 个结果。用户 ID,您需要的任何其他信息。 如果您正在跨表进行复杂查询或为每个连接添加待办事项,where 子句的每个部分等。 如果您正在使用这些项目,请添加用于订购和分页等的项目。

先选择最简单的东西。一次只做一件小事(在一个红-绿-重构循环中)。在处理列表时,您可能希望将项目分解成更小的部分,或者您可能会想到需要做的其他事情。将它们添加到 TODO 列表中,而不是直接处理它们。

在这种特殊情况下,我会在每个红-绿-重构周期之后交换到 SQL 环境 and/or sqlite 集成测试以找出如何使下一个部分工作。我猜这是介于红色和绿色之间的一种步骤 - 选择接下来要测试的内容,编写测试(显然失败了),fiddle 大约 SQL 直到你知道如何让它通过,编写 nHibernate 调用以使您的测试绿色化,然后重构。

请注意,您列出的一些事情可能 运行 不是必需的,或者花费的时间太长等。最好还是把它们写下来,这样您就知道哪些事情做得不好你在做什么。专注于你的目标。

我也倾向于开发一个 "smells" and/or 重构列表,我认为我会想做,但还没有为这个周期做好准备。请记住最小化 duplication/refactor 测试以及 SUT(被测系统)。

这是一种做而不是看的事情。您最终进行的单元测试列表以及它们执行的代码并不能很好地描述旅程。 Kent Beck 的 TDD 原著很薄,会给你一些很好的整体指导,但不是关于构造查询的。

这些有帮助吗?

Since the most common approach is to use in memory database for this kind of operations. Should I write integration test?

使用内存数据库仍然是一个集成测试(因为它实际上测试您的查询是否生成正确的 SQL 并针对数据库执行它,see)。

If I am using in memory db can I write Unit tests?

不,这将是一个集成测试

Is one test is enough?

可能不是,您应该检查查询的每个条件,例如每个 where 子句一个测试,一个用于分页,一个用于排序(如果适用)。

Since my integration test probably will check projection, how should I name it? "GetUserForPrinting_return_correct_DTO" seems too abstract and silly.

GivenUserForPrinting_WhenGetUserForPrinting_ThenMapToDTO 会更好命名