单元测试和数据库:实际上我应该在哪一点连接到数据库?


37

对于如何连接到数据库的测试类,例如“应该服务测试类如何连接...”“单元测试-数据库耦合应用程序”,存在一个问题的答案。

因此,简而言之,假设您有一个需要连接到数据库的类A。您不给A实际连接,而是给A提供了A可以用来连接的接口。为了进行测试,您需要使用一些东西来实现此接口-当然不需要连接。如果类B实例化A,则必须将“真实”数据库连接传递给A。但这意味着B打开数据库连接。这意味着要测试B,您需要将连接注入B。但是B是在类C中实例化的,依此类推。

因此,在什么时候我必须说“在这里,我从数据库中获取数据,而我不会为这段代码编写单元测试”?

换句话说:我必须调用某个类的代码中的某个地方sqlDB.connect()或类似的地方。我如何测试这堂课?

并且与必须处理GUI或文件系统的代码是否相同?


我想做单元测试。任何其他类型的测试都与我的问题无关。我知道我只会用它来测试一门课(我同意你的基利安)。现在,某些类必须连接到数据库。如果我想测试该类并询问“我该怎么做”,许多人会说:“使用依赖注入!” 但这只会将问题转移到另一类,不是吗?所以我问,如何测试真正建立连接的类?

奖励问题:这里的一些答案归结为“使用模拟对象!” 这意味着什么?我模拟了被测类所依赖的类。我现在应该模拟被测类并实际测试该模拟(这与使用模板方法的想法很接近,见下文)?


您要测试的是数据库连接吗?在内存数据库中创建临时数据库(例如derby)是否可以接受?

@MichaelT仍然我必须用实际数据库替换内存DB中的临时数据库。哪里?什么时候?如何进行单元测试?还是可以不对代码进行单元测试?
TobiMcNamobi 2013年

3
关于数据库,没有什么可“单元测试”的。它由其他人维护,并且如果其中存在错误,则必须让他们修复它,而不是自己解决。类的实际使用与测试期间的使用之间唯一的区别应该是数据库连接的参数。属性文件读取代码,Spring注入机制或用于将应用程序编织在一起的任何东西都不太可能损坏(并且如果是的话,您无法自己对其进行修复,请参见上文)-因此我认为可以接受不要测试这种管道功能。
Kilian Foth,

2
@KilianFoth与工作环境和员工角色完全相关。它实际上与问题没有任何关系。如果没有人负责数据库怎么办?
Reactgular 2013年

一些模拟框架允许您将模拟对象注入几乎所有对象,甚至注入私有成员和静态成员。这使得使用模拟数据库连接之类的东西进行测试变得非常容易。最近,Mockito + Powermock对我有用(他们是Java,不确定您在做什么)。
FrustratedWithFormsDesigner 2013年

Answers:


21

单元测试的重点是测试一个类(实际上,它通常应该测试一种方法)。

这意味着当您测试class时A,您将在其中插入一个测试数据库-一些自写的数据库或快速的内存数据库,无论完成什么工作。

但是,如果您测试class B,它是的客户端A,则通常会A用其他东西模拟整个对象,大概是某些东西以原始的,预先编程的方式完成其工作-不使用实际A对象,当然也不使用数据base(除非A将整个数据库连接传递回给其调用方-但这太可怕了,我不想考虑它)。同样,当您为class C的客户端编写类的单元测试时B,您会嘲笑充当角色的东西B,而A完全忘记了。

如果您不这样做,那么它将不再是单元测试,而是系统或集成测试。这些也很重要,但是完全不同。首先,它们通常需要更多的精力来设置和运行,要求将它们作为签入的先决条件等是不可行的。


11

对数据库连接执行单元测试是完全正常的,并且是一种惯例。根本不可能创建一种purist方法,使系统中的所有内容都可以依赖注入。

这里的关键是针对临时数据库或仅测试数据库进行测试,并具有构建该测试数据库的最轻巧的启动过程。

对于CakePHP的单元测试,有一些称为fixtures。夹具是为单元测试动态创建的临时数据库表。夹具具有创建它们的便捷方法。他们可以从测试数据库内部的生产数据库中重新创建模式,或者您可以使用简单的符号定义模式。

取得成功的关键是不实现业务数据库,而仅专注于要测试的代码方面。如果您有一个验证数据模型仅读取已发布文档的单元测试,则该测试的表模式应仅具有该代码所需的字段。您不必为了测试该代码而重新实现整个内容管理数据库。

一些其他参考。

http://en.wikipedia.org/wiki/Test_fixture

http://phpunit.de/manual/3.7/en/database.html

http://book.cakephp.org/2.0/en/development/testing.html#fixtures


28
我不同意。需要数据库连接的测试不是单元测试,因为该测试本质上会产生副作用。这并不意味着您不能编写自动化测试,而是从定义上说,这种测试是一种集成测试,它在代码库之外行使系统的某些区域。
KeithS 2013年

5
称我为纯粹主义者,但我坚信单元测试不应执行任何会离开测试运行时环境“沙盒”的操作。他们不应该接触数据库,文件系统,网络套接字等。这是出于多种原因,其中最重要的原因是测试对外部状态的依赖性。另一个是性能;您的单元测试套件应快速运行,并且与这些外部数据存储接口的连接速度将降低几个数量级。在我自己的开发中,我使用部分模拟来测试诸如存储库之类的东西,并且我很乐意为沙箱定义“边缘”。
KeithS 2013年

2
@gbjbaanb-一开始听起来不错,但以我的经验来说非常危险。即使在架构最好的测试套件和框架中,回滚此事务的代码也可能不会执行。如果测试运行程序崩溃或在测试中中止,或者测试抛出SOE或OOME,最好的情况是您在数据库中有一个挂起的连接和事务,它将锁定您触摸的表,直到连接被杀死。防止这种导致问题的方法(例如将SQLite用作测试数据库)有其自身的缺点,例如,您实际上并没有真正在使用真正的数据库。
KeithS 2013年

5
@KeithS我认为我们正在就语义展开辩论。这与单元测试或集成测试的定义无关。我使用夹具来测试依赖于数据库连接的代码。如果那是一个集成测试,那我就可以了。我需要知道测试通过了。我不在乎依赖,性能或风险。除非该测试通过,否则我不知道该代码是否有效。对于大多数测试,没有依赖关系,但是对于那些存在的依赖关系,则这些依赖关系不能解耦。说它们应该很容易,但是根本不能。
Reactgular 2013年

4
我想我们也是。我也为集成测试使用了“单元测试框架”(NUnit),但我确实确保将这两类测试分开(通常在单独的库中)。我要说明的一点是,您的单元测试套件应该完全可隔离,以便您可以运行每天进行几次这些测试,而不会踩到同事的脚趾。
KeithS

4

在代码库中的某处,有一行代码执行连接到远程数据库的实际操作。该代码行(十分之九)是对特定于您的语言和环境的运行时库提供的“内置”方法的调用。因此,它不是“您的”代码,因此您不需要对其进行测试;为了进行单元测试,您可以相信此方法调用将正确执行。您可以并且应该仍然在单元测试套件中进行测试的是诸如确保用于此调用的参数与您期望的参数一样的事情,例如确保连接字符串正确,SQL语句或存储过程名称。

这是单元测试不应离开运行时“沙箱”并依赖于外部状态的限制的目的之一。实际上很实用;单元测试的目的是验证您编写的(或将要在TDD中编写的)代码是否按预期的方式运行。您未编写的代码(例如用于执行数据库操作的库)不应作为任何单元测试范围的一部分,这是因为您没有编写代码非常简单。

在您的集成测试套件中,这些限制得到了放松。现在你可以设计接触数据库的测试,以确保您编写的代码与未编写的代码能很好地配合使用。但是,这两个测试套件应该保持隔离,因为您的单元测试套件运行得更快,因此效率更高(因此您可以快速验证开发人员对其代码的所有断言仍然成立),并且按照定义,集成测试由于增加了对外部资源的依赖性,因此速度降低了几个数量级。让构建机器人每隔几个小时运行一次完整的集成套件,执行测试以锁定外部资源,这样开发人员就不会通过在本地运行这些相同的测试来踩对方的脚。如果构建中断,那又如何呢?确保构建机器人永远不会使构建失败的重要性远远超过应有的重要性。


现在,您能否严格遵守此规则取决于您连接和查询数据库的确切策略。在许多情况下,您必须使用“裸露的”数据访问框架,例如ADO.NET的SqlConnection和SqlStatement对象,由您开发的整个方法可能由内置方法调用和其他依赖于具有数据库连接,因此在这种情况下,您最好的办法是模拟整个功能并信任您的集成测试套件。它还取决于您设计类的意愿,以允许出于测试目的而替换特定的代码行(例如Tobi对Template Method模式的建议,这是一个很好的选择,因为它允许“部分模拟”

如果您的数据持久性模型依赖于数据层中的代码(例如触发器,存储的proc等),那么除了开发驻留在数据层内部或跨数据层的测试之外,别无选择地行使您自​​己正在编写的代码应用程序运行时与DBMS之间的边界。出于这个原因,纯粹主义者会说要避免这种模式,而应该使用ORM之类的东西。我认为我不会走那么远。即使在语言集成查询和其他经过编译器检查,依赖于域的持久性操作的时代,我也看到了将数据库锁定为仅通过存储过程公开的操作的价值,当然,必须使用自动方法来验证此类存储过程测试。但是,此类测试不是单元测试。他们是整合 测试。

如果您对此区分有疑问,通常是基于对完整的“代码覆盖率”或“单元测试覆盖率”的高度重视。您想确保单元测试覆盖代码的每一行。脸上有一个崇高的目标,但我要说“洗碗”。这种心态适合于反模式拉伸远远超出了这个特定的情况下,如执行,但不写assertionless测试演习您的代码。仅出于覆盖范围的目的,这些类型的最终运行比放宽最低覆盖范围更有害。如果您要确保代码库的每一行都通过某种自动化测试执行,那么这很容易;在计算代码覆盖率指标时,请包括集成测试。您甚至可以更进一步,隔离这些有争议的“ Itino”测试(“仅名称中的集成”),并且在您的单元测试套件和集成测试的这一子类别(仍应运行得相当快)之间,您应该会感到头疼几乎接近全覆盖。


2

单元测试永远不要连接到数据库。根据定义,他们应该测试每个单元的单一代码(一种方法),使其与系统的其余部分完全隔离。如果没有,则它们不是单元测试。

除了语义学之外,还有很多原因可以使之受益:

  • 测试运行速度快几个数量级
  • 反馈环路变得瞬间(例如,TDD的反馈<1s)
  • 可以针对构建/部署系统并行运行测试
  • 测试不需要运行数据库(使构建更容易,或者至少更快)

单元测试是检查工作的一种方式。他们应该概述给定方法的所有场景,这通常意味着方法的所有不同路径。这是您要建立的规范,类似于两次进入簿记。

您要描述的是另一种自动测试:集成测试。尽管它们也很重要,但理想情况下,它们的数量要少得多。他们应验证一组单元是否正确集成。

那么,如何通过数据库访问来测试事物呢?您所有的数据访问代码都应位于特定的层中,以便您的应用程序代码可以与可模拟服务(而不是实际数据库)进行交互。不管这些服务是否由任何类型的SQL数据库,内存中的测试数据甚至是远程Web服务数据来支持。这不是他们的关注。

理想情况下(这是非常主观的),您希望单元测试覆盖大部分代码。这使您确信每个部分都可以独立工作。一旦构建完成,就需要将它们放在一起。示例-当我对用户密码进行哈希处理时,我应该获得此确切的输出。

假设每个组件大约由5个类组成-您需要测试它们中的所有故障点。这就意味着要进行更少的测试,只是为了确保所有内容都正确接线。示例-测试您可以从给定用户名/密码的数据库中找到用户。

最后,您需要一些验收测试以真正确保您达到业务目标。这些甚至更少了。他们可以确保应用程序正在运行,并且可以完成其构建工作。示例-给定此测试数据,我应该能够登录。

将这三种类型的测试视为金字塔。您需要大量的单元测试来支持所有内容,然后再逐步进行。


1

模板方法模式可能会有所帮助。

您可以在protected方法中包装对数据库的调用。要测试该类,您实际上要测试一个假对象,该对象从真实的数据库连接类继承并覆盖受保护的方法。

这样,对数据库的实际调用就不会在单元测试中进行,是正确的。但这只是这几行代码。这是可以接受的。


1
如果您想知道为什么我回答我自己的问题:是的,这可能是一个答案,但是我不确定这是否是正确的答案。
TobiMcNamobi

-1

使用外部数据进行测试是集成测试。单元测试意味着您仅在测试单元。这主要是由您的业务逻辑完成的。为了使您的代码单元可测试,您必须遵循一些准则,例如,必须使您的代码单元独立于代码的其他部分。在单元测试期间,如果需要数据,则需要通过依赖项注入来强制注入该数据。有一些模拟和存根框架。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.