(数据库)集成测试不好吗?


120

有人坚持认为集成测试是种种种错误和错误的方法 -一切都必须经过单元测试,这意味着您必须模拟依赖项;由于种种原因,我并不总是喜欢这种选择。

我发现在某些情况下,单元测试根本无法证明任何事情。

让我们以以下(简单,幼稚的)存储库实现(在PHP中)为例:

class ProductRepository
{
    private $db;

    public function __construct(ConnectionInterface $db) {
        $this->db = $db;
    }

    public function findByKeyword($keyword) {
        // this might have a query builder, keyword processing, etc. - this is
        // a totally naive example just to illustrate the DB dependency, mkay?

        return $this->db->fetch("SELECT * FROM products p"
            . " WHERE p.name LIKE :keyword", ['keyword' => $keyword]);
    }
}

假设我想在测试中证明该存储库实际上可以找到与各种给定关键字匹配的产品。

缺少与真实连接对象的集成测试,我如何才能知道这实际上是在生成真实查询-并且那些查询实际上按照我认为的方式工作?

如果必须在单元测试中模拟连接对象,则只能证明“它会生成预期的查询”之类的东西-但这并不意味着它实际上就可以工作 ……也就是说,也许它正在生成查询我预料到了,但也许该查询没有执行我认为的操作。

换句话说,我感觉像是一个对生成的查询进行断言的测试,基本上没有价值,因为它正在测试findByKeyword()方法的实现方式,但这并不能证明它实际上是有效的

这个问题不仅限于存储库或数据库集成-似乎在许多情况下都适用,在这种情况下,对使用模拟(test-double)的使用进行断言只能证明事情的实现方式,而不是它们是否将要实现。实际工作。

您如何处理此类情况?

在这种情况下,集成测试真的“不好”吗?

我的观点是,最好测试一件事,并且我也理解为什么集成测试会导致无数的代码路径,而所有这些代码路径都无法测试-但就服务(例如存储库)而言,其唯一目的是要与另一个组件交互,如何在没有集成测试的情况下真正测试任何东西?


5
阅读agitar.com/downloads/TheWayOfTestivus.pdf,尤其是第6页“测试比单元更重要”。
布朗

2
@ user61852在说明中说“天真”,是吗?
mindplay.dk,2015年

4
您的同事将如何完全确定其模拟数据库的行为与真实情况相同?
托尔比约恩Ravn的安徒生

12
您正在尝试变得现实。您的同事正在尝试遵守规则。始终编​​写能够产生价值的测试。不要浪费时间编写无法维护的测试,也不要编写不执行以下任一操作的测试:增加代码正确的可能性,或者强迫您编写更具可维护性的代码。
jpmc26 2015年

3
@ mindplay.dk:该段中的关键句子是“但不要卡在任何教条上。编写需要编写的测试。”您的同事似乎被卡在教条中。您不需要某人在您的示例中向您解释需要测试的内容-您已经知道了。很明显,要测试数据库是否理解查询,您必须对真实数据库运行查询-没有模拟可以告诉您这一点。
布朗博士

Answers:


129

您的同事是对的,应该对所有可以进行单元测试的内容进行单元测试是正确的,并且对的,单元测试可以带您进入更远的地方,尤其是在围绕复杂的外部服务编写简单的包装时,这是正确的。

关于测试的一种常见思考方式是测试金字塔。这个概念经常与敏捷相关,许多人都写过有关它的文章,包括Martin Fowler(他将其归因于Agile成功案例中的 Mike Cohn ),Alistair ScottGoogle Testing Blog

        /\                           --------------
       /  \        UI / End-to-End    \          /
      /----\                           \--------/
     /      \     Integration/System    \      /
    /--------\                           \----/
   /          \          Unit             \  /
  --------------                           \/
  Pyramid (good)                   Ice cream cone (bad)

这个概念是,快速运行的弹性单元测试是测试过程的基础-应该有比系统/集成测试更集中的单元测试,而有比端到端测试更多的系统/集成测试。随着您越接近顶端,测试往往会花费更多的时间/资源来运行,往往会遭受更大的脆弱性和脆弱性的考验,并且在确定哪个系统或文件被破坏时的针对性就不那么强了;自然地,最好避免“头重脚轻”。

到目前为止,集成测试还不错,但是严重依赖它们可能表明您没有将单个组件设计为易于测试。请记住,这里的目标是测试您的单元是否在性能指标上达到最低限度,同时涉及最少的其他易碎系统:您可能想尝试一个内存数据库(我将其视为对单元测试友好的测试,并与模拟进行了双重测试) ),例如进行繁重的案例测试,然后使用真实的数据库引擎编写一些集成测试,以确保在组装系统时主要案例都能正常工作。


作为附带说明,您提到您编写的模拟程序只是测试某种东西的实现方式,而不是它是否有效。那是一种反模式:一个完美体现其实现的测试实际上根本没有测试任何东西。取而代之的是,测试每个类或方法是否在要求的抽象或真实级别上均按照其自己的规范运行


13
+1表示“可以完美反映其实现的测试实际上根本没有测试任何东西。” 太普遍了。我将此称为Doppelganger反模式
dodgethesteamroller 2015年

6
一项由上下文驱动的测试运动进行的反向测试软件QA,部分地致力于争论是否存在诸如“测试金字塔”之类的有用的通用经验法则。尤其是开创性的 文本运动给许多例子集成测试是远远高于其他类型的测试更有价值(因为他们测试上下文中的系统,作为系统)....
dodgethesteamroller

7
因此,... Fowler等人认为,由于集成测试和用户接受测试太难以至于无法以健壮和可维护的方式编写,因此他们应该花更少的精力,实际上只是为事后解释提供了理由。他们还没有弄清楚如何在更高的水平上进行良好的测试。
dodgethesteamroller 2015年

1
@dodgethesteamroller像这样的“反向派”的深入讨论可能最适合他们自己的答案。就个人而言,我发现Google Testing Blog很好地描述了快速,范围严格的自动化测试以及系统内测试的优点。如果您不清楚,我将测试金字塔列为有用的模型或起点,而不是作为停止思考工程师的借口。
杰夫·鲍曼

1
强烈建议您介绍单元测试与集成测试的层次结构和比率:vimeo.com/80533536解释很好。
szalski 2015年

88

我的一位同事坚持认为集成测试是种种种坏事-所有的事情都必须经过单元测试,

这有点像说抗生素不好-一切都应该用维生素治愈。

单元测试不能涵盖所有内容-它们仅测试组件在受控环境中的工作方式。集成测试验证一切工作在一起,这是很难做,但更有意义到底。

一个好的,全面的测试过程会同时使用两种类型的测试-单元测试以验证业务规则和其他可以独立测试的事物,以及集成测试以确保一切正常。

缺少与真实连接对象的集成测试,我如何才能知道这实际上是在生成真实查询-并且那些查询实际上按照我认为的方式工作?

可以在数据库级别对其进行单元测试。使用各种参数运行查询,然后查看是否获得预期的结果。授予它意味着将任何更改复制/粘贴回“ true”代码中。但它确实允许您独立于任何其他依赖项来测试查询。


您是否要测试数据库是否包含某些数据?
图兰斯·科尔多瓦

可能-但您也可能正在测试过滤器,复杂联接等是否正常工作。示例查询可能不是“单元测试”的最佳选择,但可能具有复杂的联接和/或聚合。
D Stanley

是的-正如我所指出的,我使用的示例很简单;一个真正的仓库可能有复杂的搜索和排序选项,例如所有的方式来使用查询生成器等
mindplay.dk

2
好的答案,但是我要补充一点,数据库应该在内存中,以确保单元测试是快速的。
2015年

3
@BЈовић:不幸的是,这可能并不总是可能的,因为不幸的是,那里没有两个兼容的DB,而且它们中的所有都不能在内存中工作。商业DB也存在许可问题(您可能没有在任何计算机上运行它的许可),...
Matthieu

17

单元测试不能解决所有缺陷。但是与其他类型的测试相比,它们的设置和重新运行成本更低。单元测试是通过中等价值和中低成本的结合来证明的。

下表显示了不同测试类型的缺陷检测率。

在此处输入图片说明

来源:McConnell撰写的Code Complete 2中的第470页


5
该数据收集于1986年。那是三十年前。1986年的单元测试已不再是今天。我会对此数据表示怀疑。更不用说,单元测试可以在bug 提交之前就对其进行检测,因此怀疑是否会报告它们。
RubberDuck

3
@RubberDuck此图表来自2006年的一本书,它基于1986、1996、2002年的数据(如果您仔细看的话)。我没有研究源中的数据收集协议,也不能说它们何时开始跟踪缺陷以及如何报告缺陷。该图表可能会过时吗?它可以。去年12月,我在一个研讨会上,讲师提到集成测试比单元测试发现的bug多(iirc的两倍)。该图表明它们大致相同。
Nick Alexeev

13

不,他们还不错。 希望应该进行单元测试和集成测试。它们在开发周期的不同阶段使用和运行。

单元测试

编译代码后,单元测试应在构建服务器上和本地运行。如果任何单元测试失败,则应该使构建失败或不提交代码更新,直到修复测试为止。我们希望将单元测试隔离的原因是,我们希望构建服务器能够在没有所有依赖项的情况下运行所有​​测试。然后,我们可以运行构建,而无需所有复杂的依赖关系,并且有很多运行非常快的测试。

因此,对于数据库,应该具有以下内容:

IRespository

List<Product> GetProducts<String Size, String Color);

现在,IRepository的实际实现将进入数据库以获取产品,但是对于单元测试,可以使用一个伪造的IRepository来模拟IRepository以根据需要运行所有测试,而无需使用actaul数据库,因为我们可以模拟各种产品列表从模拟实例返回并使用模拟数据测试任何业务逻辑。

整合测试

集成测试通常是跨界测试。我们希望在部署服务器(真实环境),沙箱甚至本地(指向沙箱)上运行这些测试。它们不在构建服务器上运行。在将软件部署到环境后,通常这些将作为部署后活动运行。可以通过命令行实用程序将它们自动化。例如,如果我们对要调用的所有集成测试进行了分类,则可以从命令行运行nUnit。它们实际上使用真实的数据库调用来调用真实的存储库。这些类型的测试有助于:

  • 环境健康稳定性就绪
  • 测试真实的东西

这些测试有时很难运行,因为我们可能还需要设置和/或拆除。考虑添加产品。我们可能想要添加产品,查询它是否已添加,然后在完成后将其删除。我们不想添加100或1000的“集成”产品,因此需要进行其他设置。

集成测试可以证明对验证环境和确保真实事物有效非常有价值。

一个应该同时拥有。

  • 对每个构建都运行单元测试。
  • 对每个部署运行集成测试。

我建议为每个构建都运行集成测试,而不必提交和推送。取决于它们需要多长时间,但是出于许多原因,保持它们快速也是一个好主意。
artbristol

@ArtBristol-通常,我们的构建服务器未配置完整的环境依赖性,因此我们无法在此处运行集成测试。但是,如果可以在那里进行集成测试,那就去做吧。我们在构建后设置了一个部署沙箱,用于集成测试以验证部署。但是每种情况都不同。
乔恩·雷诺

11

数据库集成测试还不错。更重要的是,它们是必需的。

您可能将应用程序分成了多个层,这是一件好事。您可以通过模拟相邻的层来隔离地测试每个层,这也是一件好事。但是,无论您创建了多少个抽象层,在某些时候都必须有一层完成肮脏的工作-实际上是在与数据库对话。除非您进行测试,否则根本不会进行测试。如果测试层ň用嘲讽层N-1 ,你正在评估假设层ň工作条件是n-1个作品。为了使它起作用,您必须以某种方式证明第0层起作用。

从理论上讲,您可以通过分析和解释生成的SQL来对测试数据库进行单元化,而即时创建测试数据库并与之对话则更加容易和可靠。

结论

当最终生成的SQL包含语法错误时,通过对您的Abstract RepositoryEthereal Object-Relational-MapperGeneric Active Record,Theoryetic Persistence层进行单元测试,可以产生什么信心?


我当时想添加与您类似的回复,但您说的更好!根据我的经验,在获取和存储数据的层上进行了一些测试,这使我免于痛苦。
Daniel Hollinrake 2015年

数据库集成测试虽然不好。您的ci / cd行中是否有可用的数据库?对我来说,这似乎很复杂。模拟数据库内容并构建一个抽象层使用它要容易得多。这不仅是一个更加优雅的解决方案,而且还尽可能快。单元测试必须快速。测试数据库会大大降低单元测试的速度,使其达到无法接受的水平。即使有成千上万的单元测试,单元测试也不应花费超过10分钟的时间。
戴维(David)

@David 您的ci / cd行中是否有可用的数据库?当然,这是非常 标准的 功能。顺便说一句,我不是提倡集成测试而不是单元测试-我是提倡将集成测试单元测试结合使用。快速的单元测试是必不可少的,但是数据库太复杂了,无法依赖具有模拟交互的单元测试。
el.pescado

@ el.pescado我必须不同意。如果您的数据库通信位于抽象层的后面,那么模拟起来真的很容易。您可以决定要返回哪个对象。同样,某些东西是标准的事实并不能使它成为一件好事。
大卫,

@David我认为这取决于您如何处理数据库。是实施细节还是系统的重要组成部分(我倾向于后者)。如果您将数据库视为愚蠢的数据存储,那么可以,您可能不需要集成测试。但是,如果数据库中存在任何逻辑-约束,触发器,外键,事务,或者您的数据层使用自定义SQL而不是普通的ORM方法,那么我觉得仅凭单元测试是不够的。
el.pescado

6

你们两个都需要。

在您的示例中,如果您正在某个条件下测试数据库,则在findByKeyword运行该方法时,您将获得数据,您希望这是一次很好的集成测试。

在使用该findByKeyword方法的任何其他代码中,您都想控制向测试馈入的内容,因此您可以为测试返回空值或正确的单词,或者模拟数据库依赖项,然后您就可以知道测试将要执行的操作接收(您将损失连接到数据库并确保其中的数据正确的开销)


6

您所引用的博客文章的作者主要关注的是集成测试可能带来的潜在复杂性(尽管它以非常有根据和明确的方式编写)。但是,集成测试不一定是不好的,并且实际上比纯单元测试更有用。它实际上取决于应用程序的上下文以及您要测试的内容。

如今,如果许多应用程序的数据库服务器出现故障,则根本无法使用。至少,在您要测试的功能的上下文中考虑一下。

一方面,如果您要测试的内容与数据库无关,或者可以使之完全不依赖于数据库,则可以编写甚至不尝试使用数据库的测试方式数据库(仅根据需要提供模拟数据)。例如,如果您要在提供网页时尝试测试某些身份验证逻辑(例如),则最好将其与数据库分离(假设您不依赖数据库进行身份验证,或者您可以相当轻松地对其进行嘲笑)。

另一方面,如果该功能直接依赖于数据库,并且在数据库不可用的情况下根本无法在实际环境中运行,则可以在数据库客户端代码中模拟数据库的功能(即使用该功能的层) DB)不一定有意义。

例如,如果您知道您的应用程序将依赖于数据库(并且可能依赖于特定的数据库系统),那么为此而对数据库行为进行模拟通常会浪费时间。数据库引擎(尤其是RDBMS)是复杂的系统。几行SQL实际上可以执行很多工作,这很难模拟(实际上,如果您的SQL查询长几行,那么您可能需要更多行Java / PHP / C#/ Python代码以在内部产生相同的结果):复制已经在数据库中实现的逻辑没有任何意义,然后检查测试代码本身将成为问题。

我不一定将其视为单元测试集成测试的问题,而是要看待测试内容的范围。单元和集成测试的总体问题仍然存在:您需要一套合理可行的测试数据和测试用例集,但是它们也足够小,可以快速执行测试。

重置数据库和重新填充测试数据的时间是要考虑的一个方面。通常,您将根据编写该模拟代码所花费的时间来评估这一点(最终也必须维护)。

要考虑的另一点是您的应用程序对数据库的依赖程度。

  • 如果您的应用程序简单地遵循CRUD模型,即您拥有一层抽象层,可以通过简单的配置设置在任何RDBMS之间进行交换,则您很可能能够轻松地使用模拟系统(可能会模糊)使用内存中的RDBMS在单元测试和集成测试之间建立界线)。
  • 如果您的应用程序使用更复杂的逻辑(例如,特定于SQL Server,MySQL,PostgreSQL的逻辑),那么使用该特定系统进行测试通常更有意义。

“如果数据库服务器出现故障,今天的许多应用程序根本无法工作”-这很重要!
el.pescado

很好地解释了复杂模拟的局限性,例如使用另一种语言模拟SQL。当测试代码变得足够复杂以致似乎需要对其进行自我测试时,这就是QA的味道。
dodgethesteamroller 2015年

1

您认为这样的单元测试不完整是正确的。不完整在于被模拟的数据库接口中。这种天真的模拟的期望或断言是不完整的。

要使其完整,您必须花费足够的时间和资源来编写或集成SQL规则引擎,以确保被测对象发出的SQL语句能够实现预期的操作。

然而,通常被遗忘的并且相对昂贵的模拟替代/伴随是“虚拟化”

您可以启动一个临时的内存中“真实”数据库实例来测试单个功能吗?是的 在那里,您有一个更好的测试,它可以检查保存和检索的实际数据。

也许有人会说,您已经将单元测试变成了集成测试。关于在单元测试和集成测试之间划分界限的位置有不同的看法。恕我直言,“单位”是一个任意定义,应符合您的需求。


1
这似乎只是重复在几个小时前发布的先前答案中提出和解释的观点
gna

0

Unit Tests并且Integration Tests彼此正交。它们为您正在构建的应用程序提供了不同的视图。通常你都想要。但是,当您需要哪种测试时,时间点有所不同。

最常想的Unit Tests。单元测试专注于所测试代码的一小部分-确切地称为a unit留给读者。但是德的目的很简单:越来越的反馈,并在那里你的代码爆发。也就是说,应该很清楚,对实际数据库的调用是nono

另一方面,有些东西只有在没有数据库的情况下才能在艰苦的条件下进行单元测试。也许您的代码中存在争用条件,并且对DB的调用引发了对a的违反,unique constraint只有当您实际使用系统时才可能抛出该违反。但是这类测试非常昂贵,您不能(也不希望)像这样频繁地运行它们unit tests


0

在.Net世界中,我习惯于创建一个测试项目,并创建测试来作为减去UI的编码/调试/测试往返方法。这是我发展的有效途径。我对运行每个构建的所有测试都不感兴趣(因为这确实减慢了我的开发工作流程),但是我知道这对于更大的团队很有用。不过,您可以制定一条规则,即在提交代码之前,应运行并通过所有测试(如果由于实际上是在命中数据库而导致测试花费更长的时间)。

模拟数据访问层(DAO)并没有实际访问数据库,不仅使我无法按照自己喜欢的方式进行编码,而且还错过了实际的大部分代码。如果您不是真正地在测试数据访问层和数据库,而是在假装,然后花大量时间进行模拟,那么我将无法掌握这种方法在实际测试代码中的用处。我正在测试一小块,而不是大块。我知道我的方法可能更像是集成测试,但是如果您实际上只是一次编写集成测试,那么使用模拟的单元测试似乎是浪费时间。这也是开发和调试的好方法。

实际上,一段时间以来,我已经了解了TDD和行为驱动设计(BDD),并思考了使用它的方法,但是很难追溯地添加单元测试。也许我是错的,但是编写一个包含更多端对端代码并包含数据库的测试似乎是一种更完整,优先级更高的测试,涵盖了更多代码,并且是编写测试的更有效方法。

实际上,我认为像行为驱动设计(BDD)这样的尝试使用领域特定语言(DSL)进行端到端测试的方法应该是可行的。我们在.Net世界中拥有SpecFlow,但它始于Cucumber的开源。

https://cucumber.io/

我编写的模拟数据访问层而不打数据库的测试的真正实用性真的没有让我印象深刻。返回的对象没有命中数据库,也没有填充数据。这是一个完全空的对象,我不得不以一种不自然的方式对其进行模拟。我只是认为这是浪费时间。

根据Stack Overflow,当实际对象不适合合并到单元测试中时,将使用模拟。

https://stackoverflow.com/questions/2665812/what-is-mocking

“模拟主要用于单元测试。被测试的对象可能与其他(复杂)对象具有依赖性。要隔离您要测试的对象的行为,可以用模拟真实对象行为的模拟代替其他对象。如果实际对象不适合合并到单元测试中,这将很有用。”

我的观点是,如果我要对端到端进行任何编码(从Web UI到业务层,从数据访问层到数据库,往返),那么在以开发人员身份签入任何内容之前,我将测试此往返流程。如果我从测试中切出UI并调试并测试此流程,那么我将测试UI之外的所有内容,并返回UI期望的结果。我剩下的就是向UI发送它想要的东西。

我有一个更完整的测试,这是我自然开发工作流程的一部分。对我来说,那应该是最高优先级的测试,它涵盖了尽可能多地测试实际用户规范。如果我再也没有创建任何其他更精细的测试,至少我可以再进行一次完整的测试来证明我所需的功能有效。

Stack Exchange的共同创始人并不相信拥有100%单元测试覆盖率的好处。我也不是。我将进行更完整的“集成测试”,该测试对数据库的影响超过了每天维护一堆数据库模拟的次数。

https://www.joelonsoftware.com/2009/01/31/from-podcast-38/


它是如此明显,你不明白单元测试和集成测试的区别
BЈовић

我认为这取决于项目。在资源较少的小型项目中,由于缺乏测试人员并使文档与代码保持同步,开发人员更全面地负责测试和回归测试,因此,如果我要花时间编写测试,那将是给了我最大的收益。我想用一块石头杀死尽可能多的鸟。如果我的大多数逻辑和错误来自于生成报告的数据库存储过程,还是来自前端JavaScript,那么在中间层进行完整的单元测试将无济于事。
user3198764

-1

应该嘲笑外部依赖性,因为您无法控制它们(它们可能在集成测试阶段通过但在生产中失败)。驱动器可能会失败,数据库连接可能由于多种原因而失败,可能会出现网络问题等。进行集成测试并不能给人任何额外的信心,因为它们都是运行时可能发生的问题。

使用真正的单元测试,您可以在沙盒的范围内进行测试,并且应该清楚。如果开发人员编写的SQL查询在QA / PROD中失败,则意味着他们在那之前甚至没有测试过一次。


“您无法控制它们(它们可能在集成测试阶段通过但在生产中失败)” + 1
图兰斯·科尔多瓦

可以控制它们达到满意的程度。
el.pescado

我明白你的意思,但是我认为这比现在更真实了吗?使用自动化和工具(如Docker),您实际上可以准确,可靠地复制和重复所有二进制/服务器依赖项的设置,以进行集成测试套件。当然,是的,物理硬件(和第三方服务等)可能会失败。
mindplay.dk

5
我绝对不同意。您应该编写(其他)集成测试,因为外部依赖项可能会失败。外部依赖项可能有其自身的怪癖,在嘲笑所有内容时您很可能会错过这些怪癖。
Paul Kertscher 2015年

1
@PaulK仍在思考标记为已接受的答案,但我倾向于相同的结论。
mindplay.dk 2015年
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.