【问题标题】:TDD: Do I have to define everything my code should NOT do?TDD:我必须定义我的代码不应该做的所有事情吗?
【发布时间】:2013-11-19 01:34:25
【问题描述】:

问题

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

我有MyObject,我想从中调用属于OtherObjectmethodA()methodB(),具体取决于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();
}

问题

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

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

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

看起来测试的数量会影响代码有多少选项。

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

【问题讨论】:

    标签: java unit-testing junit tdd


    【解决方案1】:

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

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

    不要回答对methodAmethodB 的呼叫,而是根据从外面可以看到的内容。 callMethod 应该返回什么(如果有的话)? callMethod 的调用者还能看到哪些额外的行为?

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

    【讨论】:

      【解决方案2】:

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

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

      总体而言,如果您的测试很复杂,这并不意味着测试无效。好的测试和好的设计是同一枚硬币的两个方面。测试需要一次解决小块问题才能有效,就像代码需要一次解决小块问题才能保持可维护性和凝聚力一样。两只手互相洗手。

      【讨论】:

        【解决方案3】:

        很好的问题。将 TDD 应用于字母(尤其是像您一样使用 Devil's Advocate 技术)确实揭示了一些有趣的问题。

        Mark Seemann 有一个关于类似问题的recent article,他证明使用不同的、稍微更严格的模拟可以解决问题。我不知道 Mockito 是否可以做到这一点,但使用 Moq 等框架,在您的示例中将 mockOtherObject 设为严格模拟会导致我们想要的异常,因为会调用未准备好的方法 methodB() .

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

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

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

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

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

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

        如果您这样做,OtherObject 就不必再有 2 个方法 - switch 语句和 Devil's Advocate 错误完全消失。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2019-01-06
          • 2012-03-31
          • 2010-10-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-11-26
          相关资源
          最近更新 更多