New to unit testing, how to write great tests?
我对单元测试领域还比较陌生,本周我刚刚决定为我现有的应用程序增加测试覆盖率。
这是一项巨大的任务,主要是因为要测试的类的数量,而且因为编写测试对我来说都是新的。
我已经为许多类编写了测试,但现在我想知道我是否做得对。
当我为一个方法编写测试时,我有第二次重写我已经在方法本身中编写的内容的感觉。我的测试似乎与方法(测试所有代码路径,期望一些内部方法被多次调用,带有某些参数)紧密地绑定在一起,以至于如果我重构该方法,即使该方法的最终行为没有改变,测试也将失败。
这只是一种感觉,正如前面所说,我没有测试的经验。如果有更多经验丰富的测试人员可以给我一些建议,如何为现有的应用程序编写伟大的测试,那将是非常感谢的。
编辑:我很想感谢StackOverflow,我在15分钟内输入了大量信息,这就回答了我刚才在网上阅读的更多时间。
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.
我认为你做错了。
单元测试应:
- 试验一法
- 为该方法提供一些特定的参数
- 测试结果是否符合预期
它不应该在方法内部看到它在做什么,所以改变内部结构不应该导致测试失败。您不应该直接测试正在调用私有方法。如果您有兴趣了解您的私有代码是否正在测试,那么使用代码覆盖工具。但是不要被这个困扰:100%的覆盖率不是一个要求。
如果您的方法在其他类中调用公共方法,并且这些调用由接口保证,那么您可以使用模拟框架测试这些调用是否正在进行。
您不应该使用方法本身(或它使用的任何内部代码)动态生成预期的结果。预期的结果应该硬编码到您的测试用例中,以便在实现更改时不会更改。下面是单元测试应该做什么的简化示例:
1 2 3 4 5 6 7 8 9 10 | testAdd() { int x = 5; int y = -2; int expectedResult = 3; Calculator calculator = new Calculator(); int actualResult = calculator.Add(x, y); Assert.AreEqual(expectedResult, actualResult); } |
请注意,计算结果的方式没有被选中-只有结果是正确的。继续添加越来越多的像上面这样简单的测试用例,直到您已经覆盖尽可能多的场景。使用代码覆盖工具查看是否遗漏了任何有趣的路径。
对于单元测试,我发现测试驱动(测试第一,代码第二)和代码第一,测试第二都非常有用。
不是写代码,而是写测试。编写代码,然后看看您认为代码应该做什么。考虑一下它的所有预期用途,然后为每个目的编写一个测试。我发现编写测试比编码本身更快,但更复杂。测试应该测试意图。同时考虑一下在测试编写阶段找到角案例的意图。当然,在编写测试时,您可能会发现少数几种用法中的一种会导致错误(这是我经常发现的,我很高兴这个错误没有破坏数据并没有被取消检查)。
然而,测试几乎就像是两次编码。实际上,我的应用程序中的测试代码(数量)比应用程序代码多。一个例子是一个非常复杂的状态机。我必须确保在向它添加了更多的逻辑之后,整个事情总是在所有以前的用例上工作。由于这些案例很难理解代码,所以我最终为这台机器准备了一个很好的测试套件,我相信即使做了更改,它也不会坏,而且这些测试也救了我几次。而当用户或测试人员发现流或角情况不明的bug时,猜猜怎么着,添加到测试中却再也没有发生过。这真的给了用户信心,我的工作,除了使整个事情超级稳定。当由于性能原因需要重新编写时,猜猜看,由于测试的缘故,它在所有输入上都按预期工作。
所有像
测试不应该是字面上测试函数foo调用了函数条3次。这是错误的。检查结果和副作用是否正确,而不是内部机制。
值得注意的是,将单元测试重新装配到现有代码中要比首先通过测试驱动代码的创建困难得多。这是处理遗留应用程序的一个大问题…如何进行单元测试?这在之前已经被问过很多次了(所以你可能会被当作一个重复的问题来结束),人们通常会在这里结束:
将现有代码移动到测试驱动开发
我选择了被接受的答案的推荐书,但除此之外,答案中还有更多的信息。
不要编写测试来完全覆盖代码。编写保证您需求的测试。您可能会发现不必要的代码路径。相反,如果它们是必要的,它们是为了满足某种需求;找到它是什么,然后测试需求(而不是路径)。
保持测试规模小:每个要求一个测试。
稍后,当您需要进行更改(或编写新代码)时,请尝试先编写一个测试。只有一个。然后,您将在测试驱动开发中迈出第一步。
单元测试是关于从函数/方法/应用程序获得的输出。结果如何产生一点都不重要,重要的是它是正确的。因此,计算对内部方法的调用的方法是错误的。我倾向于做的是坐下来写一个方法在给定的输入值或特定的环境下应该返回什么,然后写一个测试,将返回的实际值与我想到的值进行比较。
在编写要测试的方法之前,尝试编写单元测试。
这肯定会迫使你对正在做的事情有一点不同的想法。你将不知道这个方法将如何工作,只知道它应该做什么。
您应该始终测试方法的结果,而不是方法如何获得这些结果。
测试应该提高可维护性。如果你改变一个方法和一个测试中断,那是件好事。另一方面,如果你把你的方法看作一个黑盒子,那么它不应该影响方法内部的内容。事实上,您需要为一些测试模拟一些东西,在这些情况下,您真的不能将该方法视为黑盒。你唯一能做的就是写一个集成测试——你加载一个被测试的服务的完全实例化实例,让它像在你的应用程序中一样运行。然后你可以把它当作一个黑盒子。
1 2 3 4 5 6 | 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. |
这是因为您在编写代码之后编写测试。如果你用另一种方法来做(先写测试),就不会有这种感觉。