我一直想知道使用存根进行单元测试与使用真实(生产)实现的一般用法,特别是在使用存根时我们是否遇到了一个相当讨厌的问题,如下所示:
假设我们有这个(伪)代码:
public class A {
public int getInt() {
if (..) {
return 2;
}
else {
throw new AException();
}
}
}
public class B {
public void doSomething() {
A a = new A();
try {
a.getInt();
}
catch(AException e) {
throw new BException(e);
}
}
}
public class UnitTestB {
@Test
public void throwsBExceptionWhenFailsToReadInt() {
// Stub A to throw AException() when getInt is called
// verify that we get a BException on doSomething()
}
}
现在假设我们稍后在编写了数百个测试时,意识到A不应该抛出AException而是抛出AOtherException。我们纠正了这个:
public class A {
public int getInt() {
if (..) {
return 2;
}
else {
throw new AOtherException();
}
}
}
我们现在已经更改了A的实现以抛出AOtherException,然后我们运行所有测试。他们过去了。什么不太好,B的单元测试通过但是错了。如果我们在此阶段将A和B放在生产中,B将传播AOtherException,因为它的实现认为A抛出AException。
如果我们在throwsBExceptionWhenFailsToReadInt测试中使用了A的实际实现,那么在更改A之后它会失败,因为B不再抛出BException。
这只是一个令人恐惧的想法,如果我们有数千个测试结构像上面的例子,我们改变了一个小东西,那么即使许多单位的行为都是错误的,所有的单元测试仍然会运行!我可能会遗漏一些东西,我希望你们中的一些聪明的人能够告诉我它是什么。
答案 0 :(得分:2)
当你说
时我们现在已经更改了A的实现以抛出AOtherException,然后我们运行所有测试。他们过去了。
我认为这是不正确的。您显然没有实现单元测试,但是B类不会捕获AException,因此不会抛出BException,因为AException现在是AOtherException。也许我错过了一些东西,但是你的单元测试不能断言在那时抛出BException吗?您需要更新类代码以正确处理AOtherException的异常类型。
答案 1 :(得分:0)
如果您更改 A类的界面,那么您的存根代码将无法构建(我假设您对生产和存根版本使用相同的头文件),您将了解它。
但是在这种情况下,您正在更改类的行为,因为异常类型实际上不是接口的一部分。每当你改变你的类的行为时,你真的必须找到所有的存根版本,并检查你是否也需要改变它们的行为。
我能想到的这个特定示例的唯一解决方案是在头文件中使用#define来定义异常类型。如果你需要将参数传递给异常的构造函数,这可能会变得混乱。
我使用的另一种技术(同样不适用于此特定示例)是从虚拟基类派生生产和存根类。这将接口与实现分开,但如果更改类的行为,仍然需要查看这两个实现。
答案 2 :(得分:0)
使用存根编写的测试没有失败是正常的,因为它旨在验证对象B与A良好通信并且可以处理来自getInt()的响应,假设getInt()抛出AException < / em>的。 不旨在检查getInt()是否真的在任何时候抛出 AException。
您可以将此类测试称为“协作测试”。
现在你需要完成的是对应的测试,它检查getInt()是否会首先抛出AException(或者AOtherException)。这是一次“合同测试”。
J B Rainsberger在合同和协作测试技术方面有很好的presentation。
通过这种技术,您可以通常去解决整个“虚假绿色测试”问题:
确定getInt()现在需要抛出AOtherException而不是AException
编写合同测试,验证getInt()在给定情况下是否抛出AOtherException
编写相应的生产代码以使测试通过
意识到你需要为合同测试进行协作测试:对于使用getInt()的每个协作者,它能否处理我们要抛出的AOtherException?
实施这些协作测试(假设您没有注意到此时已经有针对AException的协作测试检查)。
编写与测试匹配的生产代码,并且在调用getInt()但不是AOtherException时意识到B已经期望AException。
请参阅现有的协作测试,其中包含存根A抛出AException并意识到它已过时,您需要删除它。
如果你刚刚开始使用这种技术,但假设你从一开始就采用它,就不会有任何真正的问题,因为你自然会做的是改变getInt()的合同测试来使它成为现实期待AOtherException,并在此之后更改相应的协作测试(黄金法则是合同测试始终与协作测试一起进行,因此随着时间的推移它变得毫无疑问)。
如果我们使用A的真实实现为我们的 throwsBExceptionWhenFailsToReadInt测试,那么它将失败 更改A之后因为B不再抛出BException。
当然,但这实际上是另一种测试 - 集成测试。集成测试验证硬币的两面:对象B是否正确处理来自对象A的响应R,和对象A是否始终以这种方式响应?当测试中使用的A的实现开始响应R'而不是R时,这样的测试失败是正常的。
答案 3 :(得分:0)
您提到的具体示例是一个棘手的例子......编译器无法捕获它或通知您。在这种情况下,您必须努力寻找所有用法并更新相应的测试。
尽管如此,这类问题应该只是测试的一小部分 - 你不能仅仅为这个角落案例摆脱好处。
另见:TDD how to handle a change in a mocked object - 在testdrivendevelopment论坛上有类似的讨论(在上面的问题中链接)。引用史蒂夫弗里曼(GOOS成名和基于交互的测试的支持者)
所有这一切都是真的。在实践中,结合一个明智的 结合更高级别的测试,我没有看到这是一个大的 问题。通常会有更大的事情要处理。
答案 4 :(得分:0)
古代线程,我知道,但我想我会补充说JUnit有一个非常方便的异常处理功能。不要在测试中执行try / catch,而是告诉JUnit您希望该类抛出某个异常。
@Test(expected=AOtherException)
public void ensureCorrectExceptionForA {
A a = new A();
a.getInt();
}
将此扩展到B类,您可以省略一些try / catch,让框架检测异常的正确用法。