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;
        }
    }
}

首先编写测试

为此,我首先编写一个测试 - 检查调用callMethod(0)时是否调用了methodA()。我使用JUnit和Mockito。

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();
    }
}

我创建了消除错误所需的类/方法,并通过实现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();
}

A. 问题

这有效,但显然不是我想要的。如何使用测试进行到我想要的代码?在这里,我已经测试了我想要的行为。我能想到的唯一解决方案是为我不想看到的行为编写更多的测试。

在此示例中,可以再编写 2 个测试来检查是否未调用其他方法,但在一般情况下,这样做肯定是一个更大的问题。当有更多的选项时,根据情况调用哪些方法和多少种不同的方法会更加复杂。

假设我的例子中有 3 个方法 - 我是否必须编写 3 个测试来检查调用了正确的方法 - 那么如果我正在检查 3 种情况中没有调用其他 2 种方法,那么还有 6 种方法?(无论你是否尝试坚持每个测试的一个断言,你仍然必须写下它们。

看起来测试的数量将取决于代码有多少选项。

另一种选择是只编写ifswitch语句,但从技术上讲,它不会由测试驱动。

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

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

不要根据打电话给methodAmethodB来回答,而是从外面可以看到什么。 应该从callMethod返回什么(如果有的话(? callMethod的调用者可以看到哪些其他行为?

如果methodA做了一些callMethod调用者可以观察到的特殊操作,那么请将其包含在您的测试中。 如果在发生callMethod(0)时观察该行为很重要,请对其进行测试。 如果重要的是不要在callMethod(1)发生时观察这种行为,那么也要测试一下

关于你的具体例子,我想说你做得完全正确。 测试应指定所测试类的行为。 如果您需要指定您的类在某些情况下不执行某些操作,那就这样吧。 在另一个例子中,这不会打扰你。 例如,在此方法中检查这两个条件可能不会引起任何异议:

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,或者它是测试;它只有一个简单的测试。 这也不是使测试更简单的唯一方法,这只是我想到的第一件事。

总的来说,如果你的测试很复杂,这并不意味着测试无效。 好的测试和好的设计是同一枚硬币的两面。 测试需要一次咬掉问题的一小部分才能有效,就像代码需要一次解决问题的一小部分才能进行维护和凝聚一样。 两只手互相洗手。

好问题。将TDD应用于字母(特别是像您那样使用魔鬼代言人技术(确实揭示了一些有趣的问题。

马克·西曼(Mark Seemann(最近有一篇关于类似问题的文章,他证明了使用不同的,稍微严格的模拟可以解决问题。我不知道 Mockito 是否可以做到这一点,但是对于 Moq 等框架,在您的示例中,将mockOtherObject设置为严格的模拟会导致我们想要的异常,因为将调用未准备好的方法methodB()

话虽如此,这仍然属于"测试你的代码不应该做的事情",我不太喜欢验证事情不会发生 - 它使你的测试变得僵化了很多。我看到的唯一例外是,如果一种方法对您的系统足够关键/危险,以证明使用防御手段来确保它不被调用是合理的,但这不应该经常发生。

现在,有些东西可能会胜过整个难题 - TDD周期的重构部分。

在该步骤中,您应该意识到switch语句有点臭味。更模块化、更解耦的方式怎么样?如果我们仔细想想,callMethod()要采取的行动实际上是由

  • MyObject 的实例化器(在构造时传递适当的OtherObject(

  • callMethod() 的调用方(传递适当的 i 参数,该参数将依赖于方法调用(

因此,另一种解决方案可能是以某种方式将传递的i与构造中注入的对象中的一个方法结合起来,以触发预期的操作(@tallseth 的工厂示例正是关于这一点的(。

如果你这样做,OtherObject不必再有 2 种方法 - switch语句和魔鬼代言人错误完全消失。

相关内容

最新更新