我使用过许多由后端复杂程度各异的数据库驱动的 Web 应用程序。通常,有一个 ORM 与业务和表示逻辑分开的层。这使得业务逻辑的单元测试变得相当简单;事物可以在离散模块中实现,并且测试所需的任何数据都可以通过对象模拟来伪造。

但测试 ORM 和数据库本身始终充满问题和妥协。

多年来,我尝试了一些策略,但没有一个让我完全满意。

  • 使用已知数据加载测试数据库。针对 ORM 运行测试并确认返回正确的数据。这里的缺点是您的测试数据库必须跟上应用程序数据库中的任何架构更改,并且可能会不同步。它还依赖于人工数据,并且可能不会暴露由于愚蠢的用户输入而发生的错误。最后,如果测试数据库很小,它不会揭示诸如缺少索引之类的低效率问题。(好吧,最后一个并不是真正应该使用单元测试的目的,但它并没有什么坏处。)

  • 加载生产数据库的副本并对其进行测试。这里的问题是,您可能不知道在任何给定时间生产数据库中有什么;如果数据随时间变化,您的测试可能需要重写。

有人指出,这两种策略都依赖于特定的数据,单元测试应该只测试功能。为此,我看到了建议:

  • 使用模拟数据库服务器,并仅检查 ORM 是否发送正确的查询来响应给定的方法调用。

您使用什么策略来测试数据库驱动的应用程序(如果有)?什么对你最有效?

有帮助吗?

解决方案

实际上,我已经使用了您的第一种方法并取得了相当大的成功,但我认为以稍微不同的方式可以解决您的一些问题:

  1. 将整个架构和用于创建它的脚本保留在源代码管理中,以便任何人都可以在签出后创建当前的数据库架构。此外,将示例数据保存在构建过程中加载的数据文件中。当您发现导致错误的数据时,请将其添加到示例数据中以检查错误不会再次出现。

  2. 使用持续集成服务器构建数据库架构、加载示例数据并运行测试。这就是我们保持测试数据库同步的方式(在每次测试运行时重建它)。虽然这要求 CI 服务器有权访问和拥有其自己的专用数据库实例,但我说每天构建 3 次数据库模式极大地帮助发现了直到交付之前(如果不是稍后)才可能发现的错误)。我不能说我在每次提交之前都会重建架构。有人吗?通过这种方法,您将不必这样做(也许我们应该这样做,但如果有人忘记了也没什么大不了的)。

  3. 对于我的团队来说,用户输入是在应用程序级别(而不是数据库)完成的,因此这是通过标准单元测试进行测试的。

加载生产数据库副本:
这是我上一份工作中使用的方法。这是由于以下几个问题造成的巨大痛苦:

  1. 该副本将比生产版本过时
  2. 将对副本的架构进行更改,并且不会传播到生产系统。此时我们会有不同的模式。不好玩。

模拟数据库服务器:
我目前的工作也是这样做的。每次提交后,我们都会针对注入了模拟数据库访问器的应用程序代码执行单元测试。然后我们每天执行三次上述完整的数据库构建。我绝对推荐这两种方法。

其他提示

我总是针对内存数据库(HSQLDB 或 Derby)运行测试,原因如下:

  • 它让您思考应在测试数据库中保留哪些数据以及原因。只需将您的生产DB拖到测试系统中,就可以转化为“我不知道我在做什么或为什么,如果有什么破裂,不是我!” )
  • 它确保可以在新位置轻松地重新创建数据库(例如,当我们需要从生产中复制错误时)
  • 它对 DDL 文件的质量有很大帮助。

一旦测试开始,内存数据库就会加载新数据,并且在大多数测试之后,我会调用 ROLLBACK 以保持其稳定。 总是 保持测试DB中的数据稳定!如果数据一直在变化,就无法测试。

数据从 SQL、模板数据库或转储/备份加载。如果转储采用可读格式,我更喜欢它们,因为我可以将它们放入 VCS 中。如果这不起作用,我会使用 CSV 文件或 XML。如果我必须加载大量数据......我不。您永远不必加载大量数据:)不适用于单元测试。性能测试是另一个问题,并且适用不同的规则。

我已经问这个问题很长时间了,但我认为没有灵丹妙药。

我目前所做的是模拟 DAO 对象,并在内存中保留一组良好的对象表示,这些对象表示可以存在于数据库中的有趣数据案例。

我认为这种方法的主要问题是,您只覆盖了与 DAO 层交互的代码,但从未测试 DAO 本身,并且根据我的经验,我发现该层上也发生了很多错误。我还保留了一些针对数据库运行的单元测试(为了在本地使用 TDD 或快速测试),但这些测试永远不会在我的持续集成服务器上运行,因为我们不为此目的保留数据库,而且我认为在 CI 服务器上运行的测试应该是独立的。

我发现非常有趣但并不总是值得的另一种方法,因为有点耗时,是在仅在单元测试中运行的嵌入式数据库上创建用于生产的相同模式。

尽管毫无疑问这种方法可以提高您的覆盖范围,但也有一些缺点,因为您必须尽可能接近 ANSI SQL,才能使其与您当前的 DBMS 和嵌入式替代品一起工作。

无论您认为什么与您的代码更相关,都有一些项目可以使它变得更容易,例如 数据库单元.

即使有工具允许您以一种或另一种方式模拟数据库(例如 乔奥QMockConnection, ,可以看出 这个答案 - 免责声明,我为 jOOQ 的供应商工作),我建议 不是 通过复杂的查询来模拟更大的数据库。

即使您只想集成测试您的 ORM,也要注意 ORM 会向您的数据库发出一系列非常复杂的查询,这些查询可能会有所不同

  • 句法
  • 复杂
  • 命令 (!)

模拟所有这些以生成合理的虚拟数据非常困难,除非您实际上在模拟中构建了一个小型数据库,该数据库解释传输的 SQL 语句。话虽如此,请使用众所周知的集成测试数据库,您可以使用众所周知的数据轻松重置该数据库,然后可以根据该数据库运行集成测试。

我使用第一个(针对测试数据库运行代码)。我看到您用这种方法提出的唯一实质性问题是架构可能不同步,我通过在数据库中保留版本号并通过脚本对每个版本增量应用更改来处理所有架构更改。

我还首先针对我的测试环境进行所有更改(包括对数据库架构),因此最终结果相反:所有测试通过后,将架构更新应用到生产主机。我还保留了一对单独的测试与测试。我的开发系统上的应用程序数据库,以便我可以在接触实际生产环境之前验证数据库升级是否正常工作。

我使用的是第一种方法,但有点不同,可以解决您提到的问题。

运行 DAO 测试所需的一切都在源代码控制中。它包括用于创建数据库的架构和脚本(docker 对此非常有用)。如果可以使用嵌入式数据库 - 我使用它是为了速度。

与其他描述的方法的重要区别在于,测试所需的数据不是从 SQL 脚本或 XML 文件加载的。所有内容(除了一些实际上恒定的字典数据)都是由应用程序使用实用程序函数/类创建的。

主要目的是让数据可供测试使用

  1. 非常接近测试
  2. 显式(使用 SQL 文件存储数据使得查看哪个测试使用了哪条数据变得非常困难)
  3. 将测试与不相关的更改隔离开来。

它基本上意味着这些实用程序允许在测试本身中以声明方式仅指定测试必需的内容,并省略不相关的内容。

为了了解它在实践中的含义,请考虑对某些 DAO 进行测试,该 DAO 与 CommentPost是由 Authors. 。为了测试此类 DAO 的 CRUD 操作,应在数据库中创建一些数据。测试看起来像:

@Test
public void savedCommentCanBeRead() {
    // Builder is needed to declaratively specify the entity with all attributes relevant
    // for this specific test
    // Missing attributes are generated with reasonable values
    // factory's responsibility is to create entity (and all entities required by it
    //  in our example Author) in the DB
    Post post = factory.create(PostBuilder.post());

    Comment comment = CommentBuilder.comment().forPost(post).build();

    sut.save(comment);

    Comment savedComment = sut.get(comment.getId());

    // this checks fields that are directly stored
    assertThat(saveComment, fieldwiseEqualTo(comment));
    // if there are some fields that are generated during save check them separately
    assertThat(saveComment.getGeneratedField(), equalTo(expectedValue));        
}

与带有测试数据的 SQL 脚本或 XML 文件相比,这有几个优点:

  1. 维护代码要容易得多(例如在许多测试中引用的某些实体中添加强制列,例如作者,不需要更改大量文件/记录,而只需更改构建器和/或工厂)
  2. 特定测试所需的数据在测试本身中描述,而不是在其他文件中描述。这种接近性对于测试的可理解性非常重要。

回滚与提交

我发现测试在执行时提交会更方便。首先,一些效果(例如 DEFERRED CONSTRAINTS) 如果提交从未发生,则无法检查。其次,当测试失败时,可以在数据库中检查数据,因为回滚不会恢复数据。

当然,这有一个缺点,即测试可能会产生损坏的数据,这将导致其他测试失败。为了解决这个问题,我尝试隔离测试。在上面的例子中,每个测试都可能创建新的 Author 并且所有其他实体都是与其相关的,因此碰撞很少发生。为了处理可能被破坏但不能表达为数据库级别约束的剩余不变量,我使用了一些编程检查来检查可能在每次测试后运行的错误条件(它们在 CI 中运行,但通常在本地关闭以提高性能)原因)。

对于基于 JDBC 的项目(直接或间接,例如JPA,EJB,...)您不能模拟整个数据库(在这种情况下,最好在真正的 RDBMS 上使用测试数据库),而只能模拟 JDBC 级别。

优点是这种方式带来的抽象,因为无论后端是什么,JDBC 数据(结果集、更新计数、警告等)都是相同的:您的产品数据库、测试数据库或只是为每个测试用例提供的一些模型数据。

通过为每种情况模拟 JDBC 连接,无需管理测试数据库(清理、一次仅进行一个测试、重新加载固定装置等)。每个模型连接都是隔离的,无需清理。每个测试用例中只提供了最少的必需装置来模拟 JDBC 交换,这有助于避免管理整个测试数据库的复杂性。

Acolyte 是我的框架,其中包含用于此类模型的 JDBC 驱动程序和实用程序: http://acolyte.eu.org .

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top