对我来说,关键的区别在于集成测试可以揭示一个功能是正常工作还是被破坏,因为它们在接近现实的场景中强调代码。他们调用一种或多种软件方法或功能并测试它们是否按预期运行。
相反,测试单个方法的单元测试依赖于(通常是错误的)假设,即软件的其余部分正常工作,因为它显式地模拟了每个依赖项。
因此,当实现某个功能的方法的单元测试为绿色时,并不意味着该功能正在工作。
假设您有这样的方法:
public SomeResults DoSomething(someInput) {
var someResult = [Do your job with someInput];
Log.TrackTheFactYouDidYourJob();
return someResults;
}
DoSomething
对您的客户来说非常重要:这是一项功能,是唯一重要的事情。这就是为什么您通常编写 Cucumber 规范来断言它:您希望验证并传达该功能是否有效。
Feature: To be able to do something
In order to do something
As someone
I want the system to do this thing
Scenario: A sample one
Given this situation
When I do something
Then what I get is what I was expecting for
毫无疑问:如果测试通过,您可以断言您正在交付一个工作功能。这就是你可以称之为商业价值的东西。
如果你想为你编写一个单元测试,DoSomething
你应该假装(使用一些模拟)其余的类和方法正在工作(即:该方法使用的所有依赖项都正常工作)并断言你的方法正在工作。
在实践中,您可以执行以下操作:
public SomeResults DoSomething(someInput) {
var someResult = [Do your job with someInput];
FakeAlwaysWorkingLog.TrackTheFactYouDidYourJob(); // Using a mock Log
return someResults;
}
您可以使用依赖注入、某些工厂方法或任何 Mock 框架或仅扩展被测类来做到这一点。
假设Log.DoSomething()
. 幸运的是,Gherkin 规范会找到它,并且您的端到端测试将失败。
该功能不起作用,因为Log
它被破坏了,而不是因为[Do your job with someInput]
它没有完成它的工作。而且,顺便说一句,[Do your job with someInput]
这是该方法的唯一责任。
另外,假设Log
用于 100 个其他功能,100 个其他类的 100 个其他方法。
是的,100 个功能会失败。但是,幸运的是,100 次端到端测试也失败了,并揭示了问题所在。而且,是的:他们说的是实话。
这是非常有用的信息:我知道我的产品坏了。这也是非常令人困惑的信息:它没有告诉我问题出在哪里。它告诉我的是症状,而不是根本原因。
然而,DoSomething
的单元测试是绿色的,因为它使用了一个假的Log
,构建为永不中断。而且,是的:这显然是在撒谎。它正在传达一个损坏的功能正在工作。怎么可能有用?
(如果DoSomething()
的单元测试失败,请确保:[Do your job with someInput]
有一些错误。)
假设这是一个类损坏的系统:
一个错误会破坏几个功能,并且几个集成测试会失败。
另一方面,同样的错误只会破坏一个单元测试。
现在,比较这两种情况。
同样的错误只会破坏一个单元测试。
- 您使用损坏的所有功能
Log
都是红色的
- 你所有的单元测试都是绿色的,只有单元测试
Log
是红色的
实际上,使用损坏功能的所有模块的单元测试都是绿色的,因为通过使用模拟,它们删除了依赖项。换句话说,它们运行在一个理想的、完全虚构的世界中。这是隔离错误并寻找它们的唯一方法。单元测试意味着模拟。如果你不嘲笑,你就不是单元测试。
区别
集成测试告诉我们什么是行不通的。但是它们对于猜测问题可能出在哪里是没有用的。
单元测试是唯一能告诉你错误到底在哪里的测试。要获取此信息,他们必须在模拟环境中运行该方法,其中所有其他依赖项都应该正确工作。
这就是为什么我认为你的句子“或者它只是一个跨越 2 个类的单元测试”在某种程度上被取代了。单元测试不应该跨越 2 个类。
这个回复基本上是我在这里写的总结:单元测试撒谎,这就是我喜欢它们的原因。