ChatGPT解决这个技术问题 Extra ChatGPT

单元测试数据库驱动应用程序的最佳策略是什么?

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

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

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

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

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

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

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

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

我认为对于唯一索引等情况,您仍然应该在测试环境中使用数据库索引。
我个人不介意这个问题,但如果我们遵守规则,这个问题不是针对 stackoverflow 的,而是针对 softwareengineering.stackexchange 网站的。
这个问题将 3 个不同的方面结合为一个问题。 1. 不同环境中的数据库同步(dev、qa、staging...) 2. 数据库性能测试 3. 单元测试 每个方面都有一些最佳实践。

I
Ivan Nevostruev

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

将整个架构和用于创建它的脚本保存在源代码管理中,以便任何人都可以在签出后创建当前的数据库架构。此外,将样本数据保存在由构建过程的一部分加载的数据文件中。当您发现导致错误的数据时,将其添加到您的示例数据中以检查错误不会再次出现。使用持续集成服务器来构建数据库模式、加载示例数据并运行测试。这就是我们保持测试数据库同步的方式(在每次测试运行时重建它)。虽然这要求 CI 服务器有权访问和拥有自己的专用数据库实例,但我说每天构建 3 次我们的 db 模式极大地帮助发现了可能直到交付之前才发现的错误(如果不是稍后)。我不能说我在每次提交之前都重建了架构。有人吗?使用这种方法,您不必这样做(也许我们应该这样做,但如果有人忘记了也没什么大不了的)。对于我的小组,用户输入是在应用程序级别(而不是数据库)完成的,因此这是通过标准单元测试进行测试的。

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

副本将从生产版本中过时 将对副本的架构进行更改,并且不会传播到生产系统。在这一点上,我们会有不同的模式。不好玩。

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


加载生产数据库副本也有安全和隐私方面的影响。一旦它变大,复制它并将其放入您的开发环境中可能是一件大事。
老实说,这是一个巨大的痛苦。我是测试新手,我还写了一个我想测试的 orm。我已经使用了您的第一种方法,但读到它不会成为测试单元。我使用特定的数据库引擎功能,所以模拟 DAO 会很困难。我认为我只使用我当前的方法,因为它有效并且其他人使用它。顺便说一句,自动化测试很摇滚。谢谢。
我管理两个不同的大型项目,其中一个是完美的方法,但在另一个项目中尝试实现它时遇到了很多麻烦。所以我认为这取决于每次执行测试时重新创建模式的难易程度,我目前正在努力为这个永远存在的问题寻找新的解决方案。
在这种情况下,使用像 Roundhouse 这样的数据库版本控制工具绝对值得 - 可以运行迁移的东西。这可以在任何数据库实例上运行,并且应该确保架构是最新的。此外,在编写迁移脚本时,还应编写测试数据——保持迁移和数据同步。
更好地使用猴子补丁和模拟,避免编写操作
A
Aaron Digulla

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

它让您思考将哪些数据保留在测试数据库中以及为什么。只需将您的生产数据库拖入测试系统就可以转化为“我不知道我在做什么或为什么,如果出现问题,那不是我!!” ;)

它确保可以在新的地方轻松地重新创建数据库(例如,当我们需要从生产中复制错误时)

它极大地帮助了 DDL 文件的质量。

一旦测试开始,内存中的数据库就会加载新的数据,在大多数测试之后,我调用 ROLLBACK 来保持它的稳定。始终保持测试数据库中的数据稳定!如果数据一直在变化,则无法进行测试。

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


速度是使用(特别是)内存数据库的唯一原因吗?
我想另一个优点可能是它的“一次性”性质——不需要自己清理;只需杀死内存数据库。 (但是还有其他方法可以实现这一点,例如您提到的 ROLLBACK 方法)
优点是每个测试都可以单独选择其策略。我们有在子线程中完成工作的测试,这意味着 Spring 将始终提交数据。
@Aaron:我们也在遵循这个策略。我想知道您断言内存模型与真实数据库具有相同结构的策略是什么?
@Guillaume:我正在从相同的 SQL 文件创建所有数据库。 H2 非常适合这一点,因为它支持主要数据库的大多数 SQL 特性。如果这不起作用,那么我使用一个过滤器,它采用原始 SQL 并将其转换为内存数据库的 SQL。
k
kolrie

我一直在问这个问题很长时间,但我认为没有灵丹妙药。

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

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

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

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

无论您认为什么与您的代码更相关,都有一些项目可能会使其更容易,例如 DbUnit


C
Community

即使有工具允许您以一种或另一种方式模拟您的数据库(例如 jOOQMockConnection,可以在 this answer 中看到 - 免责声明,我为 jOOQ 的供应商工作),我建议 < em>不模拟具有复杂查询的大型数据库。

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

句法

复杂

命令 (!)

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


D
Dave Sherohman

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

我还首先针对我的测试环境进行了所有更改(包括对数据库模式的更改),所以它最终是相反的:在所有测试通过后,将模式更新应用到生产主机。我还在我的开发系统上保留了一对单独的测试与应用程序数据库,以便我可以在那里验证数据库升级是否正常工作,然后再接触真正的生产盒。


c
cchantep

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

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

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

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


R
Roman-Stop RU aggression in UA

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

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

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

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

非常接近测试显式(使用 SQL 文件存储数据使得查看哪个数据被哪个测试使用非常有问题)将测试与不相关的更改隔离开来。

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

要了解它在实践中的含义,请考虑对某些 DAO 进行的测试,该测试与 Authors 编写的 CommentPost 一起使用。为了测试此类 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 文件相比,这有几个优点:

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

回滚与提交

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

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


如果您使用实体和 orm 而不是 sql 脚本为数据库播种,则它还具有以下优点:如果您对模型进行更改,编译器将强制您修复种子代码。仅当您当然使用静态类型语言时才相关。
所以澄清一下:您是在整个应用程序中使用实用程序函数/类,还是仅用于测试?
@Ella 这些实用程序功能通常在测试代码之外不需要。以 PostBuilder.post() 为例。它为帖子的所有必需属性生成一些值。这在生产代码中是不需要的。