单元测试新手,如何编写出色的测试? [关闭]
Posted
技术标签:
【中文标题】单元测试新手,如何编写出色的测试? [关闭]【英文标题】:New to unit testing, how to write great tests? [closed] 【发布时间】:2011-03-16 13:59:06 【问题描述】:我是单元测试领域的新手,本周我刚决定为我现有的应用添加测试覆盖率。
这是一项艰巨的任务,主要是因为要测试的类数量众多,还因为编写测试对我来说是全新的。
我已经为一堆类编写了测试,但现在我想知道我是否做得对。
当我为一个方法编写测试时,我有第二次重写我已经在方法本身中编写的内容的感觉。 我的测试似乎与方法紧密相关(测试所有代码路径,期望使用某些参数多次调用某些内部方法),似乎如果我重构该方法,测试将失败,即使该方法的最终行为没有改变。
这只是一种感觉,如前所述,我没有测试经验。如果一些更有经验的测试人员可以就如何为现有应用编写出色的测试提供建议,我们将不胜感激。
编辑:我要感谢 Stack Overflow,我在不到 15 分钟的时间里有很好的投入,回答了我刚刚完成的更多在线阅读时间。
【问题讨论】:
这是最好的单元测试书籍:manning.com/osherove 它解释了单元测试的所有最佳实践、注意事项。 所有这些答案遗漏的一件事是单元测试就像文档。因此,如果您编写一个函数,您将通过描述其输入和输出(以及可能的副作用)来记录其意图。那么,单元测试旨在验证这一点。如果您(或其他人)后来对代码进行了更改,文档应该解释可以进行哪些更改的边界,并且单元测试会确保保持边界。 【参考方案1】:我的测试似乎与方法紧密相关(测试所有代码路径,期望某些内部方法被多次调用,并带有某些参数),似乎如果我重构该方法,测试将失败即使方法的最终行为没有改变。
我认为你做错了。
单元测试应该:
测试一种方法 为该方法提供一些特定的参数 测试结果是否符合预期它不应该在方法内部查看它在做什么,因此更改内部不应该导致测试失败。您不应该直接测试正在调用的私有方法。如果您有兴趣了解您的私有代码是否正在测试,请使用代码覆盖率工具。但不要沉迷于此:不需要 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);
请注意,不检查结果的计算方式 - 仅检查结果是否正确。继续添加越来越多的简单测试用例,直到你已经涵盖了尽可能多的场景。使用您的代码覆盖率工具查看您是否遗漏了任何有趣的路径。
【讨论】:
非常感谢,您的回答更完整。我现在更好地理解了模拟对象的真正用途:我不需要断言对其他方法的每次调用,只需要相关的方法。我也不需要知道事情是如何完成的,但他们是否正确地完成了。 我恭敬地认为你做错了。单元测试是关于代码执行流程(白盒测试)。黑盒测试(您所建议的)通常是功能测试(系统和集成测试)中使用的技术。 “单元测试应该测试一种方法”我实际上不同意。单元测试应该测试一个逻辑概念。虽然这通常表示为一种方法,但并非总是如此 对每种方法的一项测试存在强烈分歧。每个要求进行一次测试要好得多。一种方法通常会导致单元测试基本上无法维护。 您可能会在industriallogic.com/blog/structure-shy-tests-with-lod中找到一些价值【参考方案2】:测试应该提高可维护性。如果您更改了一个方法并且测试中断,那 可能 是一件好事。另一方面,如果您将您的方法视为一个黑匣子,那么方法内部的内容无关紧要。事实是您需要为某些测试模拟事物,在这些情况下,您真的不能将该方法视为黑匣子。你唯一能做的就是编写一个集成测试——你加载一个完全实例化的被测服务实例,让它像在你的应用程序中运行一样做它的事情。然后你可以把它当作一个黑匣子。
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.
这是因为您是在编写代码之后编写测试。如果你反过来(先写测试)就不会这样了。
【讨论】:
感谢黑盒示例,我没想过。我希望我能早点发现单元测试,但不幸的是,事实并非如此,我被 legacy 应用程序添加测试所困。是否有任何方法可以将测试添加到现有项目中而不会感到损坏? 在之后编写测试与之前编写测试不同,所以你会坚持下去。但是,您可以做的是设置测试,使它们首先失败,然后通过将您的类置于测试中来使它们通过......做类似的事情,在测试最初失败后将您的实例置于测试中。与模拟相同 - 最初模拟没有期望,并且会失败,因为被测方法将对模拟做一些事情,然后使测试通过。如果您以这种方式发现很多错误,我不会感到惊讶。 另外,请务必具体说明您的期望。不要断言测试返回一个对象,测试该对象是否具有各种值。测试当一个值应该为空时,它是。在添加一些测试之后,您还可以通过进行一些您打算做的重构来分解它。【参考方案3】:单元测试是关于您从函数/方法/应用程序中获得的输出。 结果如何产生并不重要,重要的是它是否正确。 因此,您计算对内部方法的调用的方法是错误的。 我倾向于做的是坐下来写一个方法在给定特定输入值或特定环境的情况下应该返回什么,然后编写一个测试,将返回的实际值与我想出的值进行比较。
【讨论】:
【参考方案4】:对于单元测试,我发现测试驱动(测试第一,代码第二)和代码第一,测试第二都非常有用。
而不是写代码,然后写测试。编写代码然后看看你认为代码应该做什么。考虑它的所有预期用途,然后为每个用途编写一个测试。我发现编写测试比编码本身更快但更复杂。测试应该测试意图。还要考虑在测试编写阶段最终找到极端案例的意图。当然,在编写测试时,您可能会发现少数用途之一会导致错误(这是我经常发现的,我很高兴这个错误没有损坏数据并且未经检查)。
然而,测试几乎就像编码两次。事实上,我的应用程序的测试代码(数量)多于应用程序代码。一个例子是一个非常复杂的状态机。我必须确保在添加更多逻辑之后,整个事情总是适用于所有以前的用例。而且由于这些案例很难通过查看代码来理解,我最终为这台机器提供了一个非常好的测试套件,我确信即使在进行更改后它也不会崩溃,并且测试拯救了我几次.当用户或测试人员发现流程中的错误或未解决的极端情况时,猜猜是什么,添加到测试中并且再也不会发生。这真的让用户对我的工作充满信心,同时让整个事情变得超级稳定。当出于性能原因必须重写它时,猜猜看,由于测试,它在所有输入上都按预期工作。
像function square(number)
这样的所有简单示例都很棒,而且可能不适合花大量时间进行测试。那些做重要业务逻辑的,这就是测试很重要的地方。测试需求。不要只测试管道。如果需求发生变化,那么猜猜看,测试也必须发生变化。
测试不应该是真正地测试函数 foo 调用函数 bar 3 次。那是错的。检查结果和副作用是否正确,而不是内部机制。
【讨论】:
不错的答案,让我相信在代码之后编写测试仍然有用且可行。 最近的一个完美例子。我有一个非常简单的功能。传递它为真,它做一件事,假它做另一件事。很简单。进行了 4 次测试,以确保该功能完成了它打算做的事情。我稍微改变一下行为。运行测试,POW 有问题。有趣的是,在使用应用程序时,问题并没有表现出来,它只是在复杂的情况下才会出现。测试用例找到了它,我省了几个小时的头痛。 “测试应该测试意图。”我认为总结一下,您应该了解代码的预期用途并确保代码可以容纳它们。它还指出了测试应该实际测试的范围,以及当您进行代码更改时,您可能暂时不会考虑该更改如何影响代码的所有规定用途 - 测试防御不满足所有预期用例的更改。【参考方案5】:不要编写测试来全面覆盖您的代码。编写保证您的要求的测试。您可能会发现不必要的代码路径。相反,如果它们是必要的,它们就是为了满足某种要求;找到它是什么并测试需求(而不是路径)。
保持测试规模小:每个需求一个测试。
稍后,当您需要进行更改(或编写新代码)时,请先尝试编写一个测试。只有一个。然后,您将迈出测试驱动开发的第一步。
【讨论】:
谢谢,对小需求只进行小测试是有意义的,一次一个。经验教训。【参考方案6】:在编写要测试的方法之前尝试编写单元测试。
这肯定会迫使你对事情的处理方式有一点不同的看法。您将不知道该方法将如何工作,只是它应该做什么。
您应该始终测试方法的结果,而不是方法如何获得这些结果。
【讨论】:
是的,我希望能够做到这一点,只是方法已经编写好了。我只是想测试它们。将来我会在方法之前编写测试。 @pixelastic 假装方法还没写好?【参考方案7】:值得注意的是,将单元测试重新安装到现有代码中远比首先通过测试驱动代码的创建要困难得多。这是处理遗留应用程序的大问题之一……如何进行单元测试?这已经被问过很多次了(所以你可能作为一个欺骗问题被关闭),人们通常会在这里结束:
Moving existing code to Test Driven Development
我支持接受的答案的书籍推荐,但除此之外,答案中还链接了更多信息。
【讨论】:
如果你先写测试或者再写测试,这都很好,但是在写测试时你要确保你的代码是可测试的,这样你就可以写测试了。你最终会经常思考“我该如何测试这个”,这本身就会导致编写更好的代码。改造测试用例总是一个很大的禁忌。很难。这不是时间问题,而是数量和可测试性问题。我现在不能找我的老板说我想为我们超过一千个表和用途编写测试用例,现在太多了,需要我一年的时间,并且忘记了一些逻辑/决策。所以不要拖延太久:P 大概接受的答案已经改变。 Linx 的回答推荐了 Roy Osherove 的单元测试艺术,manning.com/osherove以上是关于单元测试新手,如何编写出色的测试? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章