TDD:我必须定义我的代码不应该做的所有事情吗?

时间:2013-11-19 01:34:26

标签: java unit-testing junit tdd

问题

我正在使用测试驱动开发,并且无法使我的测试很好地定义我的代码。我的问题的一个简单示例如下。

我有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}}语句,但从技术上讲,它不会受到测试的驱动。

3 个答案:

答案 0 :(得分:4)

我认为您需要对代码进行更大幅度的了解。不要考虑它应该调用什么方法,而是考虑这些方法的整体效果应该是什么。

  • 调用callMethod(0)的输出和副作用应该是什么?
  • 调用callMethod(1)的输出和副作用应该是什么?

并且不要回复methodAmethodB的来电,而是从外部可以看到的内容。应该从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完全消失。