【问题标题】:Splitting a test to a set of smaller tests将测试拆分为一组较小的测试
【发布时间】:2011-02-09 19:46:41
【问题描述】:

我希望能够将大测试拆分为小测试,这样当小测试通过时,它们暗示大测试也会通过(因此没有理由运行原始大测试)。我想这样做是因为较小的测试通常需要更少的时间、更少的精力并且更不脆弱。我想知道是否有测试设计模式或验证工具可以帮助我以稳健的方式实现这种测试拆分。

我担心当有人更改较小测试集中的某些内容时,较小测试与原始测试之间的联系会丢失。另一个担心是较小的测试集并不能真正涵盖大的测试。

我的目标示例:

//Class under test
class A {

  public void setB(B b){ this.b = b; }

  public Output process(Input i){
    return b.process(doMyProcessing(i));
  }

  private InputFromA doMyProcessing(Input i){ ..  }

  ..

}

//Another class under test
class B {

   public Output process(InputFromA i){ .. }

  ..

}

//The Big Test
@Test
public void theBigTest(){
 A systemUnderTest = createSystemUnderTest(); // <-- expect that this is expensive

 Input i = createInput();

 Output o = systemUnderTest.process(i); // <-- .. or expect that this is expensive

 assertEquals(o, expectedOutput());
}

//The splitted tests

@PartlyDefines("theBigTest") // <-- so something like this should come from the tool..
@Test
public void smallerTest1(){
  // this method is a bit too long but its just an example..
  Input i = createInput();
  InputFromA x = expectedInputFromA(); // this should be the same in both tests and it should be ensured somehow
  Output expected = expectedOutput();  // this should be the same in both tests and it should be ensured somehow

  B b = mock(B.class);
  when(b.process(x)).thenReturn(expected);

  A classUnderTest = createInstanceOfClassA();
  classUnderTest.setB(b);

  Output o = classUnderTest.process(i);

  assertEquals(o, expected);
  verify(b).process(x);
  verifyNoMoreInteractions(b);
}

@PartlyDefines("theBigTest") // <-- so something like this should come from the tool..
@Test
public void smallerTest2(){
  InputFromA x = expectedInputFromA(); // this should be the same in both tests and it should be ensured somehow
  Output expected = expectedOutput();  // this should be the same in both tests and it should be ensured somehow

  B classUnderTest = createInstanceOfClassB();

  Output o = classUnderTest.process(x);

  assertEquals(o, expected);
}

【问题讨论】:

    标签: unit-testing testing automated-tests formal-methods formal-verification


    【解决方案1】:

    我将提出的第一个建议是将您的测试重新考虑为红色(失败)。为此,您必须暂时中断生产代码。这样,您就知道测试仍然有效。

    一种常见的模式是为每个“大”测试集合使用单独的测试夹具。您不必坚持“一个测试类中的一个类的所有测试”模式。如果一组测试彼此相关,但与另一组测试无关,则将它们放在自己的类中。

    使用单独的类来为大测试保存单独的小测试的最大优势是您可以利用设置和拆卸方法。在你的情况下,我会移动你评论过的行:

    // this should be the same in both tests and it should be ensured somehow

    设置方法(在 JUnit 中,用@Before 注释的方法)。如果您有一些异常昂贵的设置需要完成,大多数 xUnit 测试框架都有一种方法来定义在所有测试之前运行一次的设置方法。在 JUnit 中,这是一个带有 @BeforeClass 注释的 public static void 方法。

    如果测试数据是不可变的,我倾向于将变量定义为常量。

    把所有这些放在一起,你可能会有这样的东西:

    public class TheBigTest {
    
        // If InputFromA is immutable, it could be declared as a constant
        private InputFromA x;
        // If Output is immutable, it could be declared as a constant
        private Output expected;
    
        // You could use 
        // @BeforeClass public static void setupExpectations()
        // instead if it is very expensive to setup the data
        @Before
        public void setUpExpectations() throws Exception {
          x = expectedInputFromA();
          expected = expectedOutput();
        }
    
        @Test
        public void smallerTest1(){
          // this method is a bit too long but its just an example..
          Input i = createInput();
    
          B b = mock(B.class);
          when(b.process(x)).thenReturn(expected);
    
          A classUnderTest = createInstanceOfClassA();
          classUnderTest.setB(b);
    
          Output o = classUnderTest.process(i);
    
          assertEquals(o, expected);
          verify(b).process(x);
          verifyNoMoreInteractions(b);
        }
    
        @Test
        public void smallerTest2(){
          B classUnderTest = createInstanceOfClassB();
    
          Output o = classUnderTest.process(x);
    
          assertEquals(o, expected);
        }
    
    }
    

    【讨论】:

    • +1 将测试保持在同一个类中(并像您一样命名测试类)将减少有人意外中断原始测试和较小测试之间的连接的可能性。谢谢你。我仍然缺少一些自动的方式来知道较小的测试意味着大的测试会通过。
    • @mkorpela,您能否详细说明为什么较小的测试意味着较大的测试会通过很重要?如果其中一项较小的测试失败,这还不足以表明存在问题吗?在任何情况下,大多数测试运行者都有整个测试类的状态指示。例如,如果类中的所有测试都通过,NUnit 和 Eclipse JUnit 测试运行器都将测试类标记为“绿色”,如果再有一个测试失败,则标记为“红色”。如果所有较小的测试都通过了,“TheBigTest”将被标记为通过。
    • @Hurme,如果原始测试在较小的测试可以通过的情况下失败,我无法删除它。
    • 这不就是在重构过程中被冲掉的东西吗?这就是为什么对红色进行重构很重要。当您退出较小的测试时,保留原始测试(并且失败)。当您拉出一个小测试时,更新生产代码以使小测试通过,然后再拉出另一个小测试。您知道一旦大测试通过,您就可以停止提取较小的测试。到时候你就可以删除原来的大测试了。
    • 这是进行测试拆分的好方法,但我应该以各种可能的方式(大数字)停止测试代码,以确保我拥有所有测试。
    【解决方案2】:

    我只能推荐这本书xUnit Test Patterns。如果有解决方案,它应该在那里。

    【讨论】:

      【解决方案3】:

      theBigTest 缺少对B 的依赖。还有smallerTest1 模拟B 依赖。在smallerTest2 你应该模拟InputFromA

      为什么要像以前一样创建依赖图?

      A 需要一个B 然后当A::process Input 时,您然后在B 中发布处理InputFromA

      保持大测试并重构 AB 以更改依赖关系映射。

      [编辑]回应评论。

      @mkorpela,我的观点是,通过查看代码及其依赖关系,您可以开始了解如何创建更小的测试。 A 依赖于 B。为了完成它的process(),它必须使用Bprocess()。因此,B 依赖于 A

      【讨论】:

      • 首先,它只是我所针对的一个非常(太)简单的例子。我正在尝试确定用于拆分测试的一般规则/工具,以便原始测试的含义仍然存在。 theBigTest 缺少对 B 的依赖,因为测试处于更抽象的级别(它不需要知道存在 B,但不知道的代价是测试将花费更长的时间来执行)。
      • 我了解简单的示例,但我的意思是,当您执行 systemUnderTest.process(i) 时,B 是一个空引用。不会跑?它可能需要您对该行为进行两个提取方法或类。它可能需要提取方法或类来测试该行为。此外,如果两个测试有可能通过,而大测试有可能失败,则需要进行第三个测试来测试失败的行为。
      • 我的 createSystemUnderTest() 方法将在组成的示例中正确初始化 systemUnderTest(假设 B 不会为空)。它与其他测试中使用的方法不同。 “如果两个测试有可能通过,而大测试有可能失败,那么你需要进行第三个测试来测试失败的行为。” - 我要问的主要问题是如何确保较小的测试能够覆盖原始测试。
      最近更新 更多