单元测试不应该使用我自己的方法吗?


83

今天,我正在观看“ JUnit基础”视频,作者说,在程序中测试给定方法时,不应在此过程中使用其他自己的方法。

更具体地说,他正在谈论测试某种记录创建方法,该方法采用参数的名称和姓氏,并使用它们在给定表中创建记录。但他声称,在测试此方法的过程中,他不应使用其他DAO方法来查询数据库以检查最终结果(以检查记录是否确实由正确的数据创建)。他声称为此,他应该编写其他JDBC代码来查询数据库并检查结果。

我想我理解他的主张的精神:您不希望一种方法的测试用例取决于另一种方法(在本例中为DAO方法)的正确性,这是通过(再次)编写自己的验证来完成的/支持代码(应该更加具体和集中,因此代码更简单)。

但是,我内心的声音开始以诸如代码重复,不必要的额外努力之类的论点来抗议。在测试其他方法时只使用其中一些方法可以吗?如果其中一个没有按预期执行操作,则其自己的测试用例将失败,我们可以对其进行修复并再次运行测试电池。无需重复代码(即使重复的代码稍微简单一些)也无需浪费精​​力。

我对此有很强烈的感触,因为我最近编写了多个Excel - VBA应用程序(感谢Rubberduck for VBA进行了正确的单元测试),其中应用此建议将意味着很多额外的工作,而没有明显的好处。

您能否分享您对此的见解?


79
这是稍微奇怪地看到,涉及到数据库中所有单元测试
理查德刺痛

4
IMO可以将其他类称为IFF,它们足够快。模拟必须进入磁盘或通过网络的任何内容。嘲笑普通的IMO类是没有意义的。
RubberDuck

2
有此视频的链接吗?
candied_orange

17
感谢Rubberduck为VBA进行了正确的单元测试 ”-先生,您今天过得很愉快。我会修正拼写错误,并将其从“ RubberDuck”编辑为“ Rubberduck”,但我觉得有些垃圾邮件发送者会这样做,并添加一个指向rubberduckvba.com的链接(我拥有域名,并与该项目共同拥有@RubberDuck)-因此,我将在这里发表评论。无论如何,这真是太棒了,看到人们实际上使用了在过去两年中大部分时间里我大部分不眠之夜都负责的工具!=)
Mathieu Guindon

4
@ Mat'sMug和RubberDuck我爱您在Rubberduck所做的事情。请继续努力。当然,因为它,我的生活更加轻松(我碰巧在Excel VBA中做了很多小程序和原型)。顺便说一句,在Rubberduck提我的文章只是我想是不错的RubberDuck自己,这又一直对我很好,这里的PE。:)
carlossierra

Answers:


186

他主张的精神确实是正确的。单元测试的重点是隔离代码,使其不受依赖地进行测试,以便可以在发生错误的地方快速识别任何错误行为。

话虽如此,单元测试是一种工具,它旨在满足您的目的,而不是为之祈祷的祭坛。有时这意味着要依赖,因为它们足够可靠地工作,并且您不想打扰它们,有时这意味着您的某些单元测试实际上非常接近,即使实际上不是集成测试。

最终,您不会对此有所评分,重要的是要测试的软件的最终产品,但是您只需要注意何时制定规则并决定何时进行权衡是值得的。


117
万岁实用主义。
罗伯特·哈维,

6
我要补充一点,可靠性并不是很多,因为速度是导致依赖关系存根的问题。孤立的口头禅的测试是如此引人注目,但是经常会忽略这一论点。
迈克尔·杜兰特

4
“ ...单元测试是一种工具,它旨在满足您的目的,不是祈祷的祭坛。”-这!
韦恩·康拉德

15
“不是要祈祷的祭坛”-愤怒的TDD教练来了!
2016年

5
@ jpmc26:更糟糕的是,您不希望所有单元测试都通过,因为它们正确处理了已关闭的Web服务,这是一种有效且正确的行为,但是从不实际执行启动代码!存根后,您可以选择是“向上”还是“向下”并进行测试。
Steve Jessop

36

我认为这归结为术语。对于许多人来说,“单元测试”是非常具体的事情,并且根据定义,不能具有通过/失败条件,该条件取决于测试单元之外的任何代码(方法,功能等)。这将包括与数据库的交互。

对其他人而言,术语“单元测试”更为宽松,涵盖了任何种类的自动化测试,包括测试应用程序集成部分的测试代码。

测试纯粹主义者(如果我可以使用该术语)可以称其为集成测试,它与单元测试有所区别,因为该测试依赖于多个纯代码单元

我怀疑您使用的是术语“单元测试”的宽松版本,实际上是在指“集成测试”。

使用这些定义,您不应在“单元测试”中依赖于被测单元之外的代码。但是,在“集成测试”中,编写一个行使某些特定代码段然后检查数据库中是否通过标准的测试是完全合理的。

是否应该依赖集成测试还是单元测试,还是两者都依赖是一个更大的讨论话题。


您猜对了。我滥用了这些术语,现在可以看到如何更适当地处理每种类型的测试。谢谢!
carlossierra

11
“对其他人来说,“单元测试”一词要宽松得多”-除了其他方面,所谓的“单元测试框架”是组织和运行更高级别测试的一种非常方便的方式;-)
Steve Jessop

1
我建议不要将“纯”与“松散”的区分充满含义,而是建议从域的角度看待“单元”,“依赖项”等的人之间的区分(例如,“身份验证”相对于从编程语言角度来看它们的人(例如,方法是单位,类是依赖项),“数据库”作为一个单元将被视为依赖项,等等。我发现,定义诸如“被测单元”之类的短语的含义可以将论点(“您错了!”)转变为讨论(“这是正确的粒度级别吗?”)。
华宝

1
@EricKing当然,对于第一类中的绝大多数人,无论您使用DAO还是直接访问数据库将数据库完全纳入单元测试的想法都是令人厌恶的。
Periata Breatta

1
@AmaniKilumanga问题基本上是“有人说我不应该在单元测试中做这个事情,但是我在单元测试中做,我认为很好。可以吗?” 我的回答是“是的,但是大多数人会称其为集成测试而不是单元测试。” 至于您的问题,我不知道“集成测试可以使用生产代码方法吗?”的意思。
埃里克·金

7

答案是是,不是。。。

隔离执行的唯一测试是绝对必须的,因为它使您能够奠定底层代码正常运行的基础。但是,在对较大的库进行编码时,您还将找到需要跨单元测试的代码区域。

这些跨单元测试非常适合代码覆盖以及测试端到端功能时的应用,但是它们确实存在一些缺点,您应该意识到:

  • 如果没有孤立的测试来确认有什么问题,失败的“跨单元”测试将需要进行其他故障排除,才能确定代码出了什么问题
  • 过多地依赖跨单元测试会使您摆脱编写SOLID面向对象代码时应始终遵循的合同思维方式。隔离测试通常很有意义,因为您的单元仅应执行一项基本操作。
  • 端到端测试是可取的,但如果测试要求您写入数据库或执行某些不想在生产环境中发生的操作,则可能会很危险。这是Mockito之类的模拟框架如此流行的众多原因之一,因为它允许您伪造对象并模仿端到端测试,而无需实际更改您不应该做的事情。

在一天结束时,您希望同时拥有这两项功能。很多测试会捕获低级功能,而有些测试会测试端到端功能。首先要关注编写测试的原因。它们在那里使您有信心代码将按预期的方式执行。您要做的任何事情都可以做到这一点。

可悲但真实的事实是,如果您完全使用自动化测试,那么您已经对许多开发人员有所帮助。当开发团队面临严峻的期限时,良好的测试实践是第一件事。因此,只要您坚持不懈地编写代码并有效地反映单元代码应该如何执行,就不会对代码的“纯粹”痴迷。


4

使用其他对象方法测试条件的一个问题是,您会错过相互抵消的错误。更重要的是,您错过了困难的测试实现的痛苦,而这种痛苦正在教会您一些有关底层代码的知识。现在,痛苦的测试意味着以后需要痛苦的维护。

使您的单元更小,拆分类,重构和重新设计,直到无需重新实现其余对象即可更轻松地进行测试。如果您认为无法进一步简化代码或测试,请寻求帮助。试想一下一个似乎总是运气好的同事,并获得易于进行干净测试的作业。向他或她寻求帮助,因为运气不好。


1
关键是单位。如果您需要致电其他程序和流程,对我来说,这不是一个单位。如果进行了多次调用并且需要验证多个步骤,那么如果它比一个单元大很多的代码,则将其分成覆盖单个逻辑的较小部分。通常,也将模拟DB调用,或者在单元测试中使用内存DB来访问实际的DB,并且需要在测试后撤消数据。
dlb

1

如果要测试的功能单元是“数据是否持久存储且可检索”,那么我将进行单元测试测试-将其存储到真实数据库中,销毁所有持有对该数据库引用的对象,然后调用代码以获取物体回来。

测试是否在数据库中创建记录似乎与存储机制的实现细节有关,而不是测试暴露给系统其余部分的功能单元。

您可能想通过使用模拟数据库来简化测试以提高性能,但是我有一些承包商完全做到了这一点,并在合同的最后留了一个通过了此类测试的系统,但实际上并未在数据库中存储任何内容系统重启之间。

您可以争论单元测试中的“单元”是指“代码单元”还是“功能单元”,这些功能可能是由许多代码单元共同创建的。我没有发现有用的区别-我要记住的问题是“测试是否告诉您有关系统是否提供业务价值的信息”,以及“如果实现发生变化,测试是否会变得脆弱?” 当您对TDD进行系统测试时,上述测试非常有用-您尚未编写“从数据库记录中获取对象”,因此尚无法测试完整的功能单元-但是对实现更改很脆弱,因此我想一旦可以测试完整操作,请删除它们。


1

精神是正确的。

理想情况下,在单元测试中,您正在测试一个单元(单个方法或小类)。

理想情况下,您将对整个数据库系统进行存根。即,您将在伪造的环境中运行您的方法,只需确保它以正确的顺序调用正确的数据库API。你明确的,肯定也不会想测试自己的方法之一,当测试数据库。

好处很多。最重要的是,由于您无需费心设置正确的数据库环境并将其回滚,因此测试很快就变得令人盲目。

当然,在任何一开始就没有做到这一点的软件项目中,这是一个崇高的目标。但这是单元测试的精神。

请注意,还有其他测试,例如功能测试,行为测试等,与该方法不同-不要将“测试”与“单元测试”混淆。


“只要确保它以正确的顺序调用正确的数据库API”就可以了。直到事实证明您对文档有误读,并且您应该使用的实际正确顺序完全不同。不要在您的控制范围之外模拟系统。
Joker_vD

4
@Joker_vD这就是集成测试的目的。在单元测试中,绝对应该模拟外部系统。
Ben Aaronson

1

至少有一个非常重要的原因,在与数据库交互的业务代码的单元测试中使用直接数据库访问:如果更改数据库实现,则必须重写所有这些单元测试。重命名一列,对于您的代码,只需更改数据映射器定义中的一行即可。但是,如果您在测试时不使用数据映射器,那么您将发现还必须更改引用此列的每个单元测试。这可能会导致大量的工作,尤其是对于那些不太适合搜索和替换的更复杂的更改。

另外,将数据映射器用作抽象机制而不是直接与数据库对话,可以更轻松地完全删除对数据库的依赖关系,这可能现在不相关了,但是当您要进行成千上万的单元测试以及所有这些在访问数据库时,您将很高兴可以轻松地重构它们以消除这种依赖关系,因为您的测试套件将从几分钟的运行时间减少到几秒钟的时间,这可以为您的生产力带来巨大的好处。

您的问题表明您正在按照正确的思路进行思考。您怀疑,单元测试也是代码。与其他代码库一样,使它们易于维护并易于适应将来的更改同样重要。力求保持它们的可读性并消除重复,您将拥有一个更好的测试套件。一个好的测试套件是可以使用的套件。使用过的测试套件有助于发现错误,而没有使用的测试套件就毫无价值。


1

在学习单元测试和集成测试时,我学到的最好的课程不是测试方法,而是测试行为。换句话说,这个对象做什么?

当我这样看时,一个持久化数据的方法和另一个读回数据的方法开始是可测试的。当然,如果要专门测试这些方法,则最终要对每种方法进行单独的测试,例如:

@Test
public void canSaveData() {
    writeDataToDatabase();
    // what can you assert - the only expectation you can have here is that an exception was not thrown.
}

@Test
public void canReadData() {
    // how do I even get data in there to read if I cannot call the method which writes it?
}

发生此问题是由于测试方法的角度。不要测试方法。测试行为。WidgetDao类的行为是什么?它保留小部件。好的,您如何验证它可以保留小部件?那么,持久性的定义是什么?这意味着当您编写它时,您可以再次读回它。因此,阅读+写作共同成为一项测试,在我看来,这是一个更有意义的测试。

@Test
public void widgetsCanBeStored() {
    Widget widget = new Widget();
    widget.setXXX.....
    // blah

    widgetDao.storeWidget(widget);
    Widget stored = widgetDao.getWidget(widget.getWidgetId());
    assertEquals(widget, stored);
}

这是一个逻辑,内聚,可靠的测试,我认为这是有意义的测试。

其他答案集中在隔离的重要性,务实与现实的争论以及单元测试是否可以查询数据库上。那些并没有真正回答这个问题。

要测试是否可以存储某些内容,您必须先存储然后再读回。如果不允许读取某些内容,则无法测试是否已存储。不要将数据存储与数据检索分开进行测试。您最终将获得任何不告诉您任何内容的测试。


非常有见地的方法,正如您所说,我认为您确实遇到了我的核心怀疑之一。谢谢!
carlossierra16年

0

您可能会抓住的一个失败案例的例子是,被测对象使用了一个缓存层,但是未能按要求持久化数据。然后,如果您查询该对象,它将说“是的,我有新的名称和地址”,但是您希望测试失败,因为它实际上并未执行应有的操作。

另外,(并忽略了违反单一职责的情况),假设需要将字符串的UTF-8编码版本持久保存在面向字节的字段中,而实际上却是Shift JIS。其他一些组件将要读取数据库,并期望看到UTF-8,因此是必需的。然后,通过该对象的往返操作将报告正确的名称和地址,因为它将从Shift JIS转换回该名称和地址,但测试未检测到错误。希望可以在以后的集成测试中检测到它,但是单元测试的全部目的是尽早发现问题,并确切地知道哪个组件负责。

如果其中一个没有按预期执行操作,则其自己的测试用例将失败,我们可以对其进行修复并再次运行测试电池。

您无法假设这一点,因为如果您不小心,则会编写一组相互依赖的测试。“是否保存?” 测试调用要测试的保存方法,然后调用加载方法以确认已保存。“加载了吗?” 测试调用save方法来设置测试夹具,然后调用要测试的加载方法来检查结果。两种测试都依赖于未测试的方法的正确性,这意味着它们均未实际测试所测试方法的正确性。

这里有一个问题的线索是,两个应该测试不同单元的测试实际上是在做同一件事。他们都先调用setter,再调用getter,然后检查结果是否为原始值。但是,您想测试设置器是否保留数据,而不是设置器/获取器对可以协同工作。因此,您知道出了点问题,您只需要找出问题并修复测试即可。

如果您的代码是为单元测试精心设计的,那么至少有两种方法可以测试被测试方法是否确实正确保存了数据:

  • 模拟数据库接口,并让模拟记录事实,即已在其上调用了正确的函数以及预期的值。这将测试该方法是否达到了预期的效果,并且是经典的单元测试。

  • 将具有相同意图的实际数据库传递给它,以记录数据是否已正确保存。但是,您的测试不是直接提供“是的,我得到了正确的数据”的模拟功能,而是直接从数据库中读出并确认它是正确的。这可能不是最纯粹的测试,因为整个数据库引擎对于编写美化的模拟程序来说是一件大事,即使有问题,我更有可能忽略使测试通过的某些细微之处(例如,我不应使用与写入时相同的数据库连接来读取,因为我可能会看到未提交的事务)。但是它测试了正确的事情,至少您知道它是精确的 实现整个数据库接口,而无需编写任何模拟代码!

因此,无论是通过JDBC从测试数据库中读取数据还是模拟数据库,都只是测试实现的细节。不管哪种方式,关键是我可以通过隔离它来更好地测试该单元,而不是允许它与同一个类上的其他不正确方法合用,即使出现问题也可以看起来正确。因此,除了信任正在测试其方法的组件之外,我想使用任何方便的方法来检查是否保留了正确的数据。

如果您的代码不是为单元测试而设计的,那么您可能别无选择,因为您要测试其方法的对象可能不接受数据库作为注入的依赖项。在这种情况下,关于隔离测试单元最佳方法的讨论变成了关于隔离测试单元的可能性的讨论。结论是一样的。如果您可以避免出现故障的单元之间发生串谋,那么您可以这样做,但要根据可用的时间以及您认为的其他任何方式来更有效地查找代码中的错误。


0

这就是我喜欢的方式。这只是个人选择,因为这不会影响产品的结果,只会影响产品的生产方式。

这取决于是否有可能在不测试应用程序的情况下在数据库中添加模拟值。

假设您有两个测试:A-读取数据库B-插入数据库(取决于A)

如果A通过,那么您可以确定B的结果将取决于B中测试的零件,而不取决于相关性。如果A失败,则B可能为假阴性。错误可能出在B或A中。直到A成功返回后,您才能确定。

我不会尝试在单元测试中编写一些查询,因为您不应该知道应用程序背后的数据库结构(否则它可能会更改)。这意味着您的测试用例可能由于您的代码本身而失败。除非您为测试编写测试,否则谁将对测试进行测试...


-1

最后,它取决于您要测试的内容。

有时足以测试您所提供的类的接口(例如,创建并存储记录,以后可以在“重新启动”之后进行检索)。在这种情况下,可以使用提供的方法进行测试(通常是一个好主意)。这样可以重用测试,例如,如果您想更改存储机制(不同的数据库,用于测试的内存数据库等)。

请注意,这意味着您只真正在乎该类是否遵守其合同(在这种情况下创建,存储和检索记录),而不是它如何实现此目的。如果您聪明地创建了单元测试(针对接口,而不是直接针对类),则可以测试不同的实现,因为它们也必须遵守接口的约定。

另一方面,在某些情况下,您必须实际验证事物的持久性(也许其他过程依赖于该数据库中格式正确的数据?)。现在,您不仅可以依靠从类中获取正确的记录,还必须验证它是否正确存储在数据库中。最直接的方法是查询数据库本身。

现在,您不仅在乎类遵守其协定(创建,存储和检索),而且在乎如何实现此协定(因为持久化数据需要遵守另一个接口)。但是,现在测试依赖于类接口存储格式,因此很难完全重用它们。(有些人可能将这类测试称为“集成测试”。)


@downvoters:您能告诉我您的原因,以便我改善您认为我的答案缺乏的方面吗?
hoffmale
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.