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

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

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

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

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

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


当前回答

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);
}

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

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

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

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

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.

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

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

Instead of writing code, then writing test. Write code then look at what you THINK the code should be doing. Think about all the intended uses of it and then write a test for each. I find writing tests to be faster but more involved than the coding itself. The tests should test the intention. Also thinking about the intentions you wind up finding corner cases in the test writing phase. And of course while writing tests you might find one of the few uses causes a bug (something I often find, and I am very glad this bug did not corrupt data and go unchecked).

Yet testing is almost like coding twice. In fact I had applications where there was more test code (quantity) than application code. One example was a very complex state machine. I had to make sure that after adding more logic to it, the entire thing always worked on all previous use cases. And since those cases were quite hard to follow by looking at the code, I wound up having such a good test suite for this machine that I was confident that it would not break even after making changes, and the tests saved my ass a few times. And as users or testers were finding bugs with the flow or corner cases unaccounted for, guess what, added to tests and never happened again. This really gave users confidence in my work in addition to making the whole thing super stable. And when it had to be re-written for performance reasons, guess what, it worked as expected on all inputs thanks to the tests.

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

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

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

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

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