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

时间:2020-03-06 14:50:19  来源:igfitidea点击:

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

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

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

  • 用已知数据加载测试数据库。针对ORM运行测试,并确认返回正确的数据。这里的缺点是测试数据库必须跟上应用程序数据库中的所有架构更改,并且可能会不同步。它还依赖于人工数据,并且可能不会暴露由于愚蠢的用户输入而发生的错误。最后,如果测试数据库很小,它将不会像索引丢失一样显示效率低下的情况。 (好吧,最后一个并不是真正的单元测试应该使用什么,但这并没有什么坏处。)
  • 加载生产数据库的副本并对此进行测试。这里的问题是,我们可能在任何给定时间都不知道生产数据库中的内容。如果数据随时间变化,则可能需要重写测试。

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

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

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

解决方案

实际上,我已经成功地使用了第一种方法,但是我认为可以解决一些问题的方式略有不同:

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

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

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

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

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

我目前要做的是模拟DAO对象,并在内存中保留大量对象的表示形式,这些对象表示可以存在于数据库中的有趣数据实例。

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

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

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

不管我们认为与代码更相关的是什么,都有一些项目可以使它变得更容易,例如DbUnit。

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

  • 它使我们可以考虑将哪些数据保留在测试数据库中以及为什么保留。只是将生产数据库拖到一个测试系统中,就会转化为"我不知道我在做什么,或者为什么以及如果有什么事情发生了,那不是我!!" ;)
  • 它确保可以轻松地在新地方重新创建数据库(例如,当我们需要从生产中复制错误时)
  • 它极大地帮助了DDL文件的质量。

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

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

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

我还首先对测试环境进行了所有更改(包括对数据库架构的更改),因此最终导致了另一种方式:在所有测试通过之后,将架构更新应用于生产主机。我还在开发系统上保留了一对单独的测试数据库和应用程序数据库,以便在接触实际生产环境之前,可以在那里验证数据库升级是否正常。