刚接触单元测试,如何编写优秀的测试?

我是单元测试领域的新手,这周我刚决定为我现有的应用程序增加测试覆盖率。

这是一项巨大的任务,主要是因为要测试的类的数量,但也因为编写测试对我来说是全新的。

我已经为很多类编写了测试,但是现在我想知道我做的是否正确。

当我在为一个方法编写测试时,我有一种重写第二次我已经在方法本身中写过的东西的感觉 我的测试似乎与方法紧密地绑定在一起(测试所有代码路径,期望一些内部方法被调用多次,带有某些参数),以至于如果我重构方法,即使方法的最终行为没有改变,测试也会失败

这只是一种感觉,正如前面所说,我没有测试的经验。如果有更有经验的测试人员能给我一些建议,告诉我如何为现有的应用程序编写出色的测试,我将不胜感激。

编辑:我要感谢Stack Overflow,我在不到15分钟的时间里得到了很好的输入,回答了我刚才做的更多小时的在线阅读。

138735 次浏览

我的测试似乎与该方法紧密地绑定在一起(测试所有代码路径,期望使用某些参数多次调用一些内部方法),以至于如果我重构该方法,即使该方法的最终行为没有改变,测试也会失败。

我认为你做错了。

一个单元测试应该:

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

它不应该在方法内部查看它正在做什么,因此改变内部内容不应该导致测试失败。您不应该直接测试私有方法是否被调用。如果您有兴趣了解您的私有代码是否正在被测试,那么可以使用代码覆盖工具。但不要被这一点所困扰: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);
}

注意,不检查计算结果的方式,只检查结果是否正确。继续像上面那样添加越来越多的简单测试用例,直到您已经覆盖了尽可能多的场景。使用代码覆盖工具查看是否遗漏了任何有趣的路径。

值得注意的是,在现有代码中重新安装单元测试比在第一个地方用测试驱动代码的创建要困难得多。这是处理遗留应用程序的一个大问题……如何进行单元测试?这个问题之前已经被问过很多次了(所以你五月 be closed as a dupe question),人们通常会在这里结束:

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

我赞同已被接受的答案的书籍推荐,但除此之外,在答案中有更多的信息链接。

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

这肯定会迫使你从不同的角度思考事情是如何做的。你不知道这个方法是如何工作的,只知道它应该做什么。

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

不要为了完全覆盖代码而编写测试。编写能够保证您的需求的测试。您可能会发现不必要的代码路径。相反,如果它们是必要的,它们的存在是为了满足某种要求;找出它是什么,并测试需求(而不是路径)。

保持测试小:每个需求一个测试。

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

单元测试是关于你从函数/方法/应用程序中得到的输出。 结果如何产生根本不重要,重要的是结果是否正确。 因此,计算内部方法调用的方法是错误的。 我倾向于坐下来,写一个方法在给定特定输入值或特定环境下应该返回什么,然后写一个测试,将实际返回的值与我想到的值进行比较

测试应该提高可维护性。如果你改变了一个方法,测试破坏了可以是一件好事。另一方面,如果你把你的方法看作一个黑盒,那么方法里面是什么就不重要了。事实上,您需要为某些测试模拟一些东西,在这些情况下,您确实不能将该方法视为一个黑盒。你唯一能做的就是写一个集成测试——你加载一个被测试服务的完全实例化实例,让它像在你的应用程序中运行一样做它的事情。然后你可以把它当作一个黑盒。

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.

这是因为您在编写代码之后才编写测试。如果你反过来做(先写测试),感觉就不会是这样了。

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

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

然而测试几乎就像编写两次代码。事实上,在我的应用程序中,测试代码(数量)比应用程序代码多。一个例子是非常复杂的状态机。我必须确保在添加了更多的逻辑之后,整个程序在之前的所有用例中都能正常工作。由于这些情况很难通过查看代码来跟踪,我最终为这台机器拥有了一个非常好的测试套件,我相信在进行更改后它不会破产,并且测试多次挽救了我的屁股。当用户或测试人员发现流程中的错误或无法解释的极端情况时,你猜怎么着,添加到测试中,并且再也没有发生过。这真的让用户对我的工作有了信心,除了让整个东西超级稳定。当由于性能原因不得不重新编写时,你猜怎么着,由于测试,它在所有输入上都能正常工作。

所有像function square(number)这样的简单示例都很好,而且可能不适合花大量时间进行测试。那些处理重要业务逻辑的程序,就是测试重要的地方。测试需求。不要只是测试管道。如果需求改变了,那么测试也必须改变。

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