单元测试的新手,如何编写出色的测试?

pix*_*tic 252 testing unit-testing

我对单元测试世界还很陌生,我刚刚决定在本周为我现有的应用添加测试覆盖率.

这是一项艰巨的任务,主要是因为要测试的课程数量,还因为编写测试对我来说都是新的.

我已经为一堆课程编写了测试,但现在我想知道我是否做得对.

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

这只是一种感觉,如前所述,我没有测试经验.如果一些更有经验的测试人员可以给我建议如何为现有应用程序编写出色的测试,那将非常感激.

编辑:我很想感谢Stack Overflow,我在15分钟内获得了很多投入,这些投入回答了我刚才在线阅读的更多时间.

Mar*_*ers 175

我的测试似乎与方法紧密相关(测试所有代码路径,期望一些内部方法被调用多次,带有某些参数),似乎如果我重构该方法,即使该方法的最终行为没有改变.

我觉得你做错了.

单元测试应该:

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

它不应该查看方法内部以查看它正在做什么,因此更改内部不应导致测试失败.您不应该直接测试正在调用的私有方法.如果您有兴趣了解您的私人代码是否正在测试,那么请使用代码覆盖工具.但不要为此着迷: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);
}
Run Code Online (Sandbox Code Playgroud)

请注意,不检查结果的计算方式 - 仅检查结果是否正确.继续添加越来越多的简单测试用例,直到您已经涵盖尽可能多的方案为止.使用代码覆盖率工具查看是否遗漏了任何有趣的路径.

  • 非常感谢,你的回答更完整.我现在更好地理解模拟对象的真正含义:我不需要断言每次调用其他方法,只需要相关的方法.我也不需要知道如何完成任务,但他们正确地做了. (10认同)
  • 我恭敬地认为_you_做错了。单元测试与代码执行流程(白盒测试)有关。黑盒测试(您的建议)通常是功能测试(系统和集成测试)中使用的技术。 (2认同)
  • “单元测试应该测试一种方法”我实际上不同意。单元测试应该测试一个逻辑概念。虽然这通常表示为一种方法,但情况并非总是如此 (2认同)
  • 对于每种方法进行一次测试存在强烈分歧。每个需求进行一次测试要好得多。每种方法一个通常会导致基本上无法维护的单元测试。 (2认同)

Dmi*_*ten 30

对于单元测试,我发现测试驱动(测试第一,代码第二)和代码优先,测试第二是非常有用.

而不是编写代码,然后编写测试.编写代码然后看看你认为代码应该做什么.考虑它的所有预期用途,然后为每个用途编写测试.我发现编写测试比编码本身更快但更复杂.测试应测试意图.还考虑了在测试编写阶段找到极端情况的意图.当然,在编写测试时,您可能会发现少数几种用法之一会导致错误(我经常发现这种错误,而且我很高兴这个错误不会破坏数据并且不加以检查).

然而,测试几乎就像编码两次.事实上,我的应用程序中存在比应用程序代码更多的测试代码(数量).一个例子是一个非常复杂的状态机.我必须确保在添加更多逻辑之后,整个事情始终适用于所有以前的用例.由于通过查看代码非常难以理解这些情况,我最终为这台机器配备了这么好的测试套件,我相信它在进行更改后不会破坏,并且测试节省了我的屁股几次.并且当用户或测试人员发现错误的流量或角落案件下落不明时,猜猜是什么,添加到测试中并且从未再次发生过.除了让整个事物变得超级稳定之外,这确实让用户对我的工作充满信心.当出于性能原因需要重新编写时,猜猜是什么,

所有简单的例子function square(number)都很棒,而且可能都是花费大量时间进行测试的不良候选人.那些做重要业务逻辑的人,那就是测试很重要的地方.测试要求.不要只是测试管道.如果要求改变然后猜测什么,测试也必须.

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

  • 很好的答案,让我相信在代码之后编写测试仍然有用且可行. (2认同)
  • 一个完美的近期例子.我有一个非常简单的功能.传递它是真的,它做一件事,假它做另一件事.非常简单.有4个测试检查,以确保该功能完成它打算做的事情.我改变了一下行为.运行测试,POW出了问题.有趣的是,当使用应用程序时问题没有显现,它只在一个复杂的情况下才会出现.测试案例找到了它,我节省了几个小时的头痛. (2认同)
  • @None我相信在说“不要测试管道”时,OP意味着如果函数被重写但具有完全相同的功能(意味着,给定相同的输入返回相同的输出),那么测试仍然应该通过。如果由于您的测试测试了内部调用函数的任意部分而导致失败,那就很糟糕了。您应该有其他测试来测试这些功能,但是单元测试应该根据需求进行,而不是根据编写代码时的工作方式进行匹配。 (2认同)

Dav*_*vid 18

值得注意的是,对现有代码进行改编的单元测试比首先通过测试驱动创建代码困难得多.这是处理遗留应用程序的一个重大问题......如何进行单元测试?之前已经多次询问过(所以你可能会因为欺骗问题被关闭),人们通常会在这里结束:

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

我接受了接受的答案的书籍推荐,但除此之外,答案中还有更多信息.

  • 如果您先编写测试或第二次测试,那么它很好,但在编写测试时,请确保您的代码是可测试的,以便您可以编写测试.你最终会想"我怎么能测试这个",这本身就会导致编写更好的代码.改造测试用例总是一个很大的禁忌.很难.它不是一个时间问题,它的数量和可测性问题.我现在不能来找我的老板说我想为我们的一千多张桌子和用途编写测试用例,现在它太多了,我需要一年时间,而且有些逻辑/决定被遗忘了.所以不要把它放得太久:P (3认同)
  • 大概答案已经改变。Linx有一个答案,建议Roy Osherove撰写的单元测试的艺术,http://www.manning.com/osherove/ (2认同)

Jon*_*eid 14

不要编写测试来全面覆盖您的代码.编写保证您要求的测试.您可能会发现不必要的代码路径.相反,如果它们是必要的,它们就是满足某种要求的; 找到它是什么并测试要求(而不是路径).

保持测试小:每个要求一次测试.

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


fre*_*oma 13

单元测试是关于从函数/方法/应用程序获得的输出.结果如何产生并不重要,重要的是它是正确的.因此,计算对内部方法的调用的方法是错误的.我倾向于做的是坐下来写一个方法应该返回给定某些输入值或某个环境,然后编写一个测试,将返回的实际值与我想出的值进行比较.


Jus*_*ner 8

在编写要测试的方法之前尝试编写单元测试.

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

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

  • @pixelastic假装没有编写方法? (2认同)

hvg*_*des 5

测试应该是为了提高可维护性。如果您更改方法并且测试中断,那可能是一件好事。另一方面,如果您将您的方法视为一个黑匣子,那么方法内部的内容应该无关紧要。事实是您需要为某些测试模拟一些东西,在这些情况下,您真的不能将方法视为黑匣子。你唯一能做的就是编写一个集成测试——你加载一个被测试服务的完全实例化的实例,让它像在你的应用程序中运行一样做它的事情。然后你可以把它当作一个黑匣子。

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.
Run Code Online (Sandbox Code Playgroud)

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

  • 之后编写测试与之前编写测试不同,因此您必须坚持下去。但是,您可以做的是设置测试,使它们首先失败,然后通过将您的类放入测试中来使它们通过......做类似的事情,在测试最初失败后将您的实例放入测试中。模拟也是如此——最初模拟没有期望,并且会失败,因为被测试的方法将用模拟做一些事情,然后使测试通过。如果您通过这种方式发现很多错误,我不会感到惊讶。 (2认同)