将测试拆分为一组较小的测试

发布于 2024-08-29 14:26:38 字数 2056 浏览 7 评论 0原文

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

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

我的目标的一个例子:

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

I want to be able to split a big test to smaller tests so that when the smaller tests pass they imply that the big test would also pass (so there is no reason to run the original big test). I want to do this because smaller tests usually take less time, less effort and are less fragile. I would like to know if there are test design patterns or verification tools that can help me to achieve this test splitting in a robust way.

I fear that the connection between the smaller tests and the original test is lost when someone changes something in the set of smaller tests. Another fear is that the set of smaller tests doesn't really cover the big test.

An example of what I am aiming at:

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

旧时模样 2024-09-05 14:26:38

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

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

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

到设置方法(在 JUnit 中,这 一个用 < 注释的方法):代码>@之前)。如果您需要完成一些异常昂贵的设置,大多数 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);
    }

}

The first suggestion that I'll make is to re-factor your tests on red (failing). To do so, you'll have to break your production code temporarily. This way, you know the tests are still valid.

One common pattern is to use a separate test fixture per collection of "big" tests. You don't have to stick to the "all tests for one class in one test class" pattern. If a a set of tests are related to each other, but are unrelated to another set of tests, then put them in their own class.

The biggest advantage to using a separate class to hold the individual small tests for the big test is that you can take advantage of setup and tear-down methods. In your case, I would move the lines you have commented with:

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

to the setup method (in JUnit, a method annotated with @Before). If you have some unusually expensive setup that needs to be done, most xUnit testing frameworks have a way to define a setup method that runs once before all of the tests. In JUnit, this is a public static void method that has the @BeforeClass annotation.

If the test data is immutable, I tend to define the variables as constants.

Putting all this together, you might have something like:

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

}
季末如歌 2024-09-05 14:26:38

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

All I can suggest is the book xUnit Test Patterns. If there is a solution it should be in there.

稀香 2024-09-05 14:26:38

theBigTest 缺少对 B 的依赖。 smallerTest1 还模拟 B 依赖项。在 smallerTest2 中,您应该模拟 InputFromA

为什么要像这样创建依赖图?

A 接受一个 B,然后当 A::process Input 时,您可以发布进程 InputFromAB 中的代码>。

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

[编辑]回应评论。

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

theBigTest is missing the dependency on B. Also smallerTest1 mocks B dependency. In smallerTest2 you should mock InputFromA.

Why did you create a dependency graph like you did?

A takes a B then when A::process Input, you then post process InputFromA in B.

Keep the big test and refactor A and B to change the dependency mapping.

[EDIT] in response to remarks.

@mkorpela, my point is that by looking at the code and their dependencies is how you start to get an idea of how to create smaller tests. A has a dependency on B. In order for it to complete its process() it must use B's process(). Because of this, B has a dependency on A.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文