确定什么是有用的单元测试


46

我一直在研究phpunit的文档,并遇到以下引号:

您可以随时编写更多测试。但是,您会很快发现,您可以想象的测试中只有一小部分确实有用。您要编写的测试即使您认为应该可以通过也要失败,或者编写的测试即使您认为应该可以通过也要成功。另一种思考的方式是成本/收益。您想编写测试以回馈信息。-埃里希·伽玛(Erich Gamma)

我很纳闷。除了该报价中关于成本/收益的内容外,如何确定使单元测试比其他单元更有用的因素。您如何决定为哪个代码创建单元测试?我之所以这么问,是因为其中的另一句话也说:

因此,如果不是关于测试,那是关于什么的?这是要弄清楚您要做什么,然后再半弯腰去尝试。您编写了一个规范,以简洁,明确和可执行的形式细化了行为的一小部分。就这么简单。这是否意味着您编写测试?不,这意味着您编写代码说明。这意味着您需要提前指定代码的行为。但时间不算早。实际上,在编写代码之前最好是最好的方法,因为那时候您掌握的信息将尽可能多。就像做得好的TDD一样,您以微小的增量工作……一次指定行为的一个小方面,然后加以实现。当您意识到这全都在于指定行为而不是编写测试时,您的观点发生了变化。突然,为您的每个生产类都提供一个Test类的想法是非常荒谬的。用自己的测试方法(以1-1关系)测试每种方法的想法将是可笑的。-戴夫·阿斯特斯

重要的部分是

* 并且用自己的测试方法(以1-1关系)测试每种方法的想法将是可笑的。*

因此,如果为每个方法创建测试都是“可笑的”,那么您如何/何时选择编写测试的目的?


5
这是一个很好的问题,但我认为这是与编程语言无关的问题-为什么将它标记为php?

这是一些非常好的文章,它们为我应该编写什么单元测试以及为哪些领域提供自动化测试提供了很好的指导。- blog.stevensanderson.com/2009/11/04/... - blog.stevensanderson.com/2009/08/24/... - ayende.com/blog/4218/scenario-driven-tests
凯恩

Answers:


26

每个方法要进行多少次测试?

理论上和高度不切实际的最大值是N-Path复杂度(假设测试全部通过代码涵盖了不同的方式;)。最小为一!。也就是说,按照公共方法,他不测试实现细节,仅测试类的外部行为(返回值和调用其他对象)。

您引用:

*并且用自己的测试方法(以1-1关系)测试每种方法的想法将是可笑的。*

然后问:

因此,如果为每个方法创建测试都是“可笑的”,那么您如何/何时选择编写测试的目的?

但是我认为您在这里误解了作者:

具有这个想法one test methodone method in the class to test就是作者所说的“可笑”。

(至少对我来说)不是关于“更少”,而是关于“更多”

因此,让我重新表达一下我对他的理解:

而且,用“ 一个方法”(它自己的测试方法以1-1的关系)测试每种方法的想法将是可笑的。

要再次引用您的报价:

当您意识到这完全是指定行为而不是编写测试时,您的观点就会改变。


练习TDD时,您不会认为

我有一个方法calculateX($a, $b);,它需要一个测试testCalculcateX来测试关于该方法的所有内容。

TDD告诉您的是考虑您的代码应该做的事情

我需要计算两个值中的较大者(第一个测试用例!),但是如果$ a小于零,则应该产生一个错误(第二个测试用例!),如果$ b小于零,则应....(第三个测试用例!),依此类推。


您要测试行为,而不只是测试没有上下文的单个方法。

这样,您将获得一个测试套件,其中包含代码文档,并真正说明了预期的功能,甚至可能是原因:)


您如何决定为哪个代码创建单元测试?

那么,存储库中或生产附近任何地方的所有内容都需要测试。我认为您的引言的作者不会不同意我在上面所做的陈述。

如果您没有测试,那么更改代码将变得更加困难(更昂贵),特别是如果不是您进行更改。

TDD是一种确保您对所有内容都进行测试的方法,但是只要您编写测试就可以了。通常在同一天编写它们会有所帮助,因为您以后不打算这样做,对吗?:)



对评论的回应:

大量的方法无法在特定的上下文中进行测试,因为它们依赖于或依赖于其他方法

这些方法可以调用三件事:

其他类的公共方法

我们可以模拟其他类,因此我们在那里定义了状态。我们可以控制上下文,因此那不是问题。

*相同的受保护或私有方法*

通常,任何不属于类的公共API的内容都不会直接进行测试。

您想测试行为而不是实现,并且如果一个类完成了所有工作,则它是在一个大型公共方法中或在许多较小的受保护方法中被称为实现的。您希望能够在不影响测试的情况下更改那些受保护的方法。因为如果您的代码更改更改行为,则测试将中断!那就是您的测试所要达到的目的,以告诉您什么时候中断某事:)

同一类上的公共方法

那不是经常发生吗?如果在以下示例中确实如此,则有几种方法可以解决此问题:

$stuff = new Stuff();
$stuff->setBla(12);
$stuff->setFoo(14);
$stuff->execute(); 

setter存在并且不属于execute方法签名的一部分是另一个主题;)

我们可以在此处测试的是,当我们设置错误的值时,execute是否会崩溃。setBla当您传递一个字符串可以进行单独测试时,这将引发异常,但是如果我们要测试这两个允许的值(12和14)不能同时起作用(无论出于何种原因),那就不是一个测试用例。

如果您想要一个“好的”测试套件,则可以在php中添加一个@covers Stuff::execute注释,以确保仅为此方法生成代码覆盖率,而其他仅用于设置的内容则需要分别进行测试(再次,如果你想要那个)。

因此,重点是:也许您需要先创建一些周围的环境,但是您应该能够编写有意义的测试用例,这些用例通常仅跨越一个或两个真实函数(此处不包括设置者)。其余的可以被以太模拟,或者先进行测试然后再依赖(请参阅参考资料@depends


*注:这个问题是从SO迁移过来的,最初是关于PHP / PHPUnit的,这就是为什么示例代码和引用来自php世界,我认为这也适用于其他语言,因为phpunit与其他xUnit并没有太大区别测试框架。


非常详细和有用的信息...您说过“您想测试行为,而不仅仅是没有上下文的单个方法。”,一定数量的方法肯定无法在特定上下文中进行测试,因为它们依赖于或依赖于其他方法,因此,有用的测试条件也只有在测试依赖项的情况下才是上下文条件?还是我误解了您的意思>
zcourts 2011年

@ robinsonc494我将在一个示例中进行编辑,或许可以更好地解释我的解决方法
edorian 2011年

感谢您的修改和示例,它肯定会有所帮助。我认为我的困惑(如果可以这样称呼)是,即使我读过关于“行为”的测试,但我还是固有地(也许是?)固有地想到了专注于实现的测试用例。
zcourts 2011年

@ robinsonc494可能是这样想的:如果您向某人拳打,他将以太拳打回去,给警察打电话或逃跑。这种行为。这就是那个人所做的。实际上,他使用贻贝的原因是他的大脑几乎没有电荷。如果您想测试某人的反应,请打他,看看他的举止是否如您所愿。您无需将他放在大脑扫描仪中,即可查看是否将脉冲发送到贻贝。有些人几乎上课了;)
edorian 2011年

3
我认为,我看到的TDD最好的例子确实是它帮助单击测试案例的方法,它是Bob Martin叔叔的保龄球比赛Kata。 slideshare.net/lalitkale/bowling-game-kata-by-robert-c-martin
Amy Anuszewski 2011年

2

测试和单元测试不是一回事。单元测试是整体测试中非常重要且有趣的子集。我认为单元测试的重点使我们以某种与上面引用相反的方式考虑这种测试。

首先,如果我们遵循TDD甚至DTH(即紧密协调地进行开发和测试),那么我们将使用编写的测试来重点关注正确的设计。通过考虑极端情况并相应地编写测试,我们避免了最初出现的错误,因此,实际上,我们编写了我们希望通过的测试(在TDD开始时就可以通过,但它们失败了,但这只是有序的人工产物,当代码已经完成,我们希望它们能够通过,而大多数情况是这样,因为您已经考虑了代码。

第二,当我们重构时,单元测试确实可以发挥作用。我们更改了实现方式,但希望答案保持不变-单元测试是我们防止违反接口合同的保护。因此,我们再次期望测试通过。

这确实意味着对于我们大概是稳定的公共接口,我们需要明确的可跟踪性,以便可以看到每个公共方法都经过测试。

要回答您的明确问题:公共接口的单元测试具有价值。

编辑回应评论:

测试私有方法?是的,我们应该这样做,但是如果我们不必测试某些东西,那就是我要妥协的地方。毕竟,如果公用方法有效,那么私有程序中的那些错误会那么重要吗?务实的是,流失往往发生在私有产品中,您需要努力维护公共界面,但是如果您依赖的事物发生变化,私有产品可能会发生变化。在某些时候,我们可能会发现维护内部测试很费力。这项工作花了吗?


因此,只是为了确保我理解您的意思:测试时,专注于测试公共接口对吗?假设这是正确的……在未进行单元测试的私有方法/接口中存在错误的可能性更大,未经测试的“私有”接口中的一些棘手错误可能会导致测试通过时通过应该真的失败了。我这样想错了吗?
zcourts 2011年

使用代码覆盖率,您可以在测试公共方法时,知道何时不执行私有方法中的代码。如果您的公共方法被完全涵盖,则任何未发现的私有方法显然都是未使用的,可以删除。如果不是,则需要对公共方法进行更多测试。
David Harkness

2

单元测试应该是更大的测试策略的一部分。在选择编写什么类型的测试以及何时编写时,我遵循以下原则:

  • 专注于编写端到端测试。与单元测试相比,每个测试覆盖的代码更多,因此,可以节省更多的测试费用。使这些成为整个系统的生死攸关的自动验证。

  • 深入到围绕复杂逻辑块编写单元测试。在端到端测试难以调试或难以编写以获得足够的代码覆盖率的情况下,单元测试很有价值。

  • 等到要测试的API稳定之后,再编写两种类型的测试。您希望避免同时重构实现和测试。

罗伯·阿什顿(Rob Ashton)在这个主题上有一篇很好的文章,我从中吸取了大量文章来阐明上述原理。


+1-我不一定同意您的所有观点(或本文的观点),但我同意的是,大多数单元测试如果盲目地进行(TDD方法)是没有用的。但是,如果您明智地决定值得花时间进行单元测试的话,它们将非常有价值。我完全同意,在编写更高级别的测试(尤其是子系统级别的自动化测试)时,您会付出更多的努力。端到端自动化测试的问题在于,如果系统具有任何大小/复杂性,对于完全不实用的系统来说,它们将非常困难。
Dunk 2013年

0

我倾向于采用一种似乎很好的单元测试方法。与其将单元测试视为“测试某种行为”,不如将其视为“我的代码必须遵循的规范”。这样,您就可以基本上声明一个对象应该以某种方式运行,并且假设您在程序的其他地方都可以肯定它是相对没有错误的。

如果您正在编写公共API,那么这是非常有价值的。但是,您也总是需要进行大量的端到端集成测试,因为达到100%的单元测试覆盖率通常是不值得的,并且会错过大多数被单元测试方法认为“令人无法接受”的事情(模拟,等等)

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.