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

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

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

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

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

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


当前回答

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

我认为你做错了。

一个单元测试应该:

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

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

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

其他回答

tests are supposed to improve maintainability. If you change a method and a test breaks that can be a good thing. On the other hand, if you look at your method as a black box then it shouldn't matter what is inside the method. The fact is you need to mock things for some tests, and in those cases you really can't treat the method as a black box. The only thing you can do is to write an integration test -- you load up a fully instantiated instance of the service under test and have it do its thing like it would running in your app. Then you can treat it as a black box.

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.

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

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

我认为你做错了。

一个单元测试应该:

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

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

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

值得注意的是,将单元测试重新安装到现有代码中要比首先用测试驱动代码的创建困难得多。这是处理遗留应用程序的一个大问题……如何进行单元测试?这个问题之前已经被问过很多次了(所以你可能会被认为是一个被骗的问题),人们通常会这样结束:

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

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

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

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

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

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

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

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