问题
我正在使用测试驱动开发,并且无法使我的测试很好地定义我的代码。我的问题的一个简单示例如下。
我有MyObject
我希望从methodA()
或methodB()
调用OtherObject
,具体取决于MyObject
在其自己callMethod(int)
中收到的参数public class MyObject {
private final OtherObject otherObject;
public MyObject(OtherObject otherObject) {
this.otherObject = otherObject;
}
public void callMethod(int i) {
switch (i) {
case 0:
otherObject.methodA();
break;
case 1:
otherObject.methodB();
break;
}
}
}
1}}。
预期代码(及所需功能)
这基本上是我想要的代码 - 但我想先做测试:
methodA()
首先编写测试
为实现这一目标,我首先编写一个测试 - 检查callMethod(0)
时调用public class MyObjectTest {
private final OtherObject mockOtherObject = mock(OtherObject.class);
private final MyObject myObject = new MyObject(mockOtherObject);
@Test
public void callsMethodA_WhenArgumentIs0() {
myObject.callMethod(0);
verify(mockOtherObject).methodA();
}
}
。我使用JUnit和Mockito。
MyObject
我创建了摆脱错误所需的类/方法,并通过实现public void callMethod(int i) {
otherObject.methodA();
}
的方法来完成测试:
callMethod(1)
接下来测试另一个选项 - 调用@Test
public void callsMethodB_WhenArgumentIs1() {
myObject.callMethod(1);
verify(mockOtherObject).methodB();
}
public void callMethod(int i) {
otherObject.methodA();
otherObject.methodB();
}
我得到了最终解决方案:
if
问题
这有效,但显然不是我想要的。如何使用测试进入我想要的代码?在这里,我测试了我想要的行为。我能想到的唯一解决方案就是为不喜欢看的行为编写更多测试。
在这个例子中,可以再编写2个测试来检查是否没有调用另一个方法,但是在一般情况下确实这样做更像是一个问题。当有更多选项时,根据具体情况调用哪些方法和调用多少种不同方法会更复杂。
假设我的例子中有3个方法 - 我是否必须编写3个测试以检查正确的方法被调用 - 如果我正在检查其他6个方法,则不会为3个案例中的每个调用? (无论你是否尝试在每次测试中坚持使用一个断言,你仍然必须全部写下来。)
看起来测试的数量将是代码有多少选项的因素。
另一种选择是只编写switch
或{{1}}语句,但从技术上讲,它不会受到测试的驱动。
答案 0 :(得分:4)
我认为您需要对代码进行更大幅度的了解。不要考虑它应该调用什么方法,而是考虑这些方法的整体效果应该是什么。
callMethod(0)
的输出和副作用应该是什么? callMethod(1)
的输出和副作用应该是什么? 并且不要回复methodA
或methodB
的来电,而是从外部可以看到的内容。应该从callMethod
返回什么(如果有的话)? callMethod
的来电者可以看到哪些其他行为?
如果methodA
执行callMethod
的调用者可以观察到的特殊内容,则将其包含在您的测试中。如果在callMethod(0)
发生时观察该行为很重要,那么请对其进行测试。如果在callMethod(1)
发生时不遵守该行为很重要,那么也要测试那个。
答案 1 :(得分:3)
关于你的具体例子,我会说你做得恰到好处。您的测试应指定您所测试的类的行为。如果你需要指明你的班级在某些情况下不做某事,那就这样吧。在另一个例子中,这不会打扰你。例如,检查此方法中的两个条件可能不会引起任何异议:
public void save(){
if(isDirty)
persistence.write(this);
}
在一般情况下,你是对的。增加方法的复杂性会使TDD更难。意想不到的结果是,这是TDD最大的 好处 之一。如果你的测试是隐藏的,那么你的代码也太复杂了。很难推理和难以维护。如果您聆听测试,则会考虑以简化测试的方式更改设计。
在你的例子中,我可能不管它(它很简单)。但是,如果case
的数量增加,我会考虑这样的改变:
public class MyObject {
private final OtherObjectFactory factory;
public MyObject(OtherObjectFactory factory) {
this.factory = factory;
}
public void callMethod(int i) {
factory.createOtherObject(i).doSomething();
}
}
public abstract class OtherObject{
public abstract void doSomething();
}
public class OtherObjectFactory {
public OtherObject createOtherObject(int i){
switch (i) {
case 0:
return new MethodAImpl();
case 1:
return new MethodBImpl();
}
}
}
请注意,此更改会为您尝试解决的问题增加一些开销;两个案例我都不打扰它。但随着案例的增长,这种情况可以很好地扩展:您为OtherObjectFactory
添加了一个新测试,并为OtherObject
添加了新的实现。你永远不会改变MyObject
或它的测试;它只有一个简单的测试。它也不是让测试更简单的唯一方法,它只是我遇到的第一件事。
总的来说,如果您的测试很复杂,并不意味着测试无效。良好的测试和良好的设计是同一枚硬币的两面。测试需要一次性地解决问题的小块,以便有效,就像代码需要一次解决问题的小块一样可维护和凝聚力。两只手互相洗漱。
答案 2 :(得分:1)
好问题。将TDD应用到字母中(特别是使用Devil的Advocate技术)确实揭示了一些有趣的问题。
Mark Seemann有一个关于类似问题的recent article,他证明使用不同的,稍微严格的模拟可以解决问题。我不知道Mockito是否能够做到这一点,但是使用Moq之类的框架,在你的示例中使mockOtherObject
严格模拟会导致我们想要的异常,因为调用了未准备的方法{{1}将被制作。
话虽如此,这仍然属于“测试你的代码不应该做的事情”,我并不是一个很好的粉丝,无法确认事情不会发生 - 它会严重影响你的测试。我看到的唯一例外是,如果一个方法对你的系统来说足够严重/危险,可以证明使用防御方法来确保它不被调用,但这不应该经常发生。
现在,某些事情可能胜过整个难题--TDD周期的重构部分。
在这一步中,你应该意识到methodB()
语句有点味道。如何采用更模块化,分离的方式?如果我们考虑一下,switch
中要采取的行动实际上由
callMethod()
的实例化程序(在构建时传递适当的MyObject
)
OtherObject
的调用方(传递相应方法调用的相应callMethod()
参数)
因此,另一种解决方案可能是以某种方式将传递的i
与在构造中注入的对象中的一个方法相结合以触发预期的操作(@tallseth的工厂示例就是这样)。
如果你这样做,i
不再需要2个方法 - OtherObject
语句和Devil's Advocate bug完全消失。