单元测试的新手,如何编写出色的测试?[关闭]


267

我对单元测试世界还很陌生,所以我决定在本周为现有应用程序增加测试范围。

这是一项艰巨的任务,主要是因为要测试的类数量众多,而且因为编写测试对我而言是全新的。

我已经为很多类编写了测试,但是现在我想知道自己是否做对了。

当我为某个方法编写测试时,我有第二次重写已经在该方法本身中编写的内容的感觉。
我的测试似乎与方法紧密绑定(测试所有代码路径,期望某些内部方法被调用多次,并带有某些参数),以至于即使我重构了该方法,即使该方法的最终行为没有改变。

这只是一种感觉,正如前面所说,我没有测试经验。如果那里有一些经验更丰富的测试人员可以给我一些有关如何为现有应用编写出色测试的建议,将不胜感激。

编辑:非常感谢Stack Overflow,我在不到15分钟的时间内就得到了很多建议,回答了我刚才所做的更多在线阅读时间。


1
这是单元测试的最佳书:manning.com/osherove它解释了单元测试的所有最佳实践,可做与不可做。
Ervi B 2010年

所有这些答案遗漏的一件事是单元测试就像文档一样。因此,如果您编写函数,则将通过描述其输入和输出(以及可能的副作用)来记录其意图。然后,单元测试就是要对此进行验证。并且,如果您(或其他人)以后对代码进行了更改,则文档应说明可以进行哪些更改的边界,并且单元测试可以确保边界得以保留。
Thomas Tempelmann

Answers:


187

我的测试似乎与方法紧密绑定(测试所有代码路径,期望某些内部方法被调用多次,并带有某些参数),以至于即使我重构了该方法,即使该方法的最终行为没有改变。

我认为您做错了。

单元测试应:

  • 测试一种方法
  • 提供该方法的一些特定参数
  • 测试结果是否符合预期

它不应查看方法内部以查看其作用,因此更改内部结构不应导致测试失败。您不应该直接测试正在调用私有方法。如果您有兴趣了解是否正在测试您的私人代码,请使用代码覆盖率工具。但是不要被这个困扰:100%的覆盖率不是必需的。

如果您的方法在其他类中调用了公共方法,并且您的接口保证了这些调用,那么您可以使用模拟框架测试是否正在进行这些调用。

您不应使用方法本身(或方法使用的任何内部代码)来动态生成预期结果。预期结果应硬编码到您的测试用例中,以便在实现发生更改时也不会更改。这是单元测试应做的简化示例:

testAdd()
{
    int x = 5;
    int y = -2;
    int expectedResult = 3;
    Calculator calculator = new Calculator();
    int actualResult = calculator.Add(x, y);
    Assert.AreEqual(expectedResult, actualResult);
}

请注意,未检查结果的计算方式-仅检查结果是否正确。像上面那样继续添加越来越多的简单测试用例,直到您涵盖了尽可能多的场景为止。使用代码覆盖率工具来查看是否错过了任何有趣的路径。


13
非常感谢,您的回答更加完整。现在,我更好地了解了模拟对象的真正用途:我不需要断言对其他方法的每次调用,而只需声明相关的方法即可。我也不需要知道如何完成工作,但是他们正确地做到了。
pixelastic 2010年

2
我恭敬地认为做错了。单元测试与代码执行流程(白盒测试)有关。黑盒测试(您的建议)通常是功能测试(系统和集成测试)中使用的技术。
Wes

1
我实际上不同意“单元测试应该测试一种方法”。单元测试应该测试一个逻辑概念。尽管通常将其表示为一种方法,但并非总是如此
robertmain

35

对于单元测试,我发现“测试驱动”(首先测试,然后是代码)和“代码首先是测试”都非常有用。

而不是编写代码,而是编写测试。编写代码,然后查看您认为代码应该做什么。考虑一下它的所有预期用途,然后为每个用途编写一个测试。我发现编写测试比编写代码要快,但要涉及的更多。测试应该测试意图。还考虑一下在编写测试阶段最终找到极端案例的意图。当然,在编写测试时,您可能会发现导致错误的几种用法之一(我经常发现的东西,我很高兴这个错误没有破坏数据并且不受检查)。

但是测试几乎就像两次编码一样。实际上,我的应用程序中测试代码(数量)比应用程序代码多。一个例子是非常复杂的状态机。我必须确保在向其中添加更多逻辑之后,整个过程始终适用于所有以前的用例。而且由于很难通过查看代码来跟踪这些情况,因此我为该机器准备了一个很好的测试套件,以至于我坚信即使进行更改,它也不会损坏,并且测试节省了我的几次钱。当用户或测试人员发现错误或无法解决的错误时,请猜测是什么,将其添加到测试中,再也不会发生。除了使整个过程超级稳定之外,这确实使用户对我的工作充满信心。当出于性能原因而不得不对其进行重写时,请猜测是什么,

所有这些简单的例子function square(number)都是很棒的,并且可能是花费大量时间测试的不佳之选。那些执行重要业务逻辑的人,那就是测试很重要的地方。测试需求。不要只是测试管道。如果需求发生变化,则猜测是什么,测试也必须如此。

测试不应从字面上测试该函数foo调用功能栏3次。那是错的。检查结果和副作用是否正确,而不是内部机制。


2
好的答案让我充满信心,在代码之后编写测试仍然有用并且可能。
pixelastic 2010年

2
最近的一个完美例子。我有一个非常简单的功能。传递它为true,它做一件事,否则为false。很简单。进行了4个测试检查,以确保该功能完成了预期的工作。我改变了一下行为。运行测试,POW出问题了。有趣的是,在使用应用程序时问题没有显现出来,只有在复杂的情况下才显现出来。测试用例发现了它,我节省了数小时的头痛。
Dmitriy Likhten,2010年

“测试应该测试意图。” 总结一下,您应该仔细检查代码的预期用途,并确保代码可以容纳它们。它还指出了测试应实际测试的范围,以及当您进行代码更改时的想法,此刻您可能不会立即考虑该更改如何影响代码的所有规定用法-测试防御无法满足所有预期用例的更改。
-Greenstick,

18

值得注意的是,将单元测试改编为现有代码是 远远比驾驶代码的创建与第一名的测试更加困难。那是处理遗留应用程序时的主要问题之一……如何进行单元测试?之前已经问过很多次了(所以您可能会被问到一个重复的问题),人们通常会在这里结束:

将现有代码移至测试驱动开发

我将接受答案的书建议列为第二,但除此之外,答案中还有更多信息链接。


3
如果您先编写测试,然后再编写测试,那么都可以,但是编写测试时,请确保代码可测试,以便您可以编写测试。您经常想到“我该如何测试”,这本身就导致编写了更好的代码。改造测试用例总是很大的禁忌。很难。它不是时间问题,而是数量和可测试性问题。我现在不能走近老板,说我想为我们的一千多个表和用途编写测试用例,现在太多了,要花我一年的时间,而一些逻辑/决定却被遗忘了。因此,不要拖延太久:P
Dmitriy Likhten

2
大概答案已经改变。Linx有一个答案,建议Roy Osherove撰写的单元测试的艺术,manning.com / osherove
thelem 2014年

15

不要编写测试来完全覆盖您的代码。编写满足您要求的测试。您可能会发现不必要的代码路径。相反,如果有必要,它们可以满足某种要求。找到它并测试需求(而不是路径)。

保持较小的测试:每个要求进行一次测试。

以后,当您需要进行更改(或编写新代码)时,请尝试首先编写一个测试。只有一个。然后,您将迈出测试驱动开发的第一步。


谢谢,有意义的是,一次只针对少量需求进行小测试。学过的知识。
pixelastic 2010年

13

单元测试是关于您从功能/方法/应用程序获得的输出的。结果的产生方式根本不重要,正确的意义也很重要。因此,您计算内部方法调用等的方法是错误的。我倾向于做的是坐下来,写下给定某些输入值或特定环境的方法应返回的内容,然后编写一个测试,将返回的实际值与我想出的结果进行比较。


谢谢 !我感觉自己做错了,但实际上有人告诉我会更好。
pixelastic 2010年

8

在编写要测试的方法之前,请尝试编写单元测试。

这肯定会迫使您对事情的完成方式有所不同。您将不知道该方法将如何工作,只知道它应该做什么。

您应该始终在测试方法的结果,而不是方法如何获得这些结果。


是的,我很希望能够做到这一点,只是方法已经编写好了。我只想测试它们。将来,我将在方法之前编写测试。
pixelastic 2010年

2
@pixelastic假装还没有编写方法?
committedandroider

4

测试应该改善可维护性。如果您改变方法并且测试中断可能是一件好事。另一方面,如果您将方法视为黑匣子,则方法内部的内容无关紧要。事实是您需要对某些测试进行模拟,在这些情况下,您实际上无法将方法视为黑盒。您唯一可以做的就是编写一个集成测试-您加载被测试服务的完全实例化的实例,并使其像在应用程序中运行一样进行。然后,您可以将其视为黑匣子。

When I'm writing tests for a method, I have the feeling of rewriting a second time what I          
already wrote in the method itself.
My tests just seems so tightly bound to the method (testing all codepath, expecting some    
inner methods to be called a number of times, with certain arguments), that it seems that
if I ever refactor the method, the tests will fail even if the final behavior of the   
method did not change.

这是因为您是在编写代码后编写测试的。如果您反过来做(首先编写测试),则不会有这种感觉。


感谢您提供的黑匣子示例,我还没有那样想。我希望我能早些发现单元测试,但是不幸的是,事实并非如此,我坚持使用旧版应用程序来添加测试。没有任何方法可以在不破坏测试的情况下将其添加到现有项目中吗?
pixelastic 2010年

1
在此之后编写测试与在此之前编写测试有所不同,因此您会受其困扰。但是,您可以做的是设置测试,以使它们首先失败,然后通过将您的类置于测试中来使它们通过....执行类似的操作,在测试最初失败后将实例置于测试中。模拟也一样-最初模拟没有期望,并且会失败,因为被测试的方法会对模拟执行某些操作,然后使测试通过。如果您以此方式发现许多错误,我不会感到惊讶。
hvgotcodes

此外,请确实符合您的期望。不要断言测试返回了一个对象,而是要测试该对象具有各种值。测试当值应该为空时是否为空。在添加一些测试之后,您还可以通过进行一些您打算做的重构来对其进行分解。
hvgotcodes
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.