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

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

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

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

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

编辑:我要感谢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.

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

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

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次。这是错误的。检查结果和副作用是否正确,而不是内部机制。

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

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

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

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

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

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

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