可靠的 Java 单元测试自动化?(JUnit/Hamcrest/...)

2022-09-01 23:47:20

意图

我正在寻找以下内容:

  • 可靠的单元测试方法
    1. 我的方法中遗漏了什么?
    2. 我做错了什么
    3. 我在做什么是不必要的?
  • 一种尽可能自动完成的方法

当前环境

目前的做法

结构

  • 每个要测试的类一个测试类
  • 分组在静态嵌套类中的方法测试
  • 测试方法命名,用于指定测试的行为 + 预期结果
  • 由 Java 注释指定的预期异常,而不是在方法名称中指定的异常

方法论

  • 注意价值null
  • 注意空列表<E>
  • 注意空字符串
  • 注意空数组
  • 注意由代码更改的对象状态不变量(后置条件)
  • 方法接受记录的参数类型
  • 边界检查(例如整数.MAX_VALUE等)
  • 通过特定类型记录不可变性(例如 Google Guava ImmutableList<E>)
  • ...有没有这个列表?可有可无的测试列表示例:
    • 在数据库项目中检查的内容(例如CRUD,连接,日志记录等)
    • 签入多线程代码的事项
    • 要检查的 EJB 的事项
    • ... ?

示例代码

这是一个人为的例子来展示一些技术。


我的路径.java

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.Arrays;
import com.google.common.collect.ImmutableList;
public class MyPath {
  public static final MyPath ROOT = MyPath.ofComponents("ROOT");
  public static final String SEPARATOR = "/";
  public static MyPath ofComponents(String... components) {
    checkNotNull(components);
    checkArgument(components.length > 0);
    checkArgument(!Arrays.asList(components).contains(""));
    return new MyPath(components);
  }
  private final ImmutableList<String> components;
  private MyPath(String[] components) {
    this.components = ImmutableList.copyOf(components);
  }
  public ImmutableList<String> getComponents() {
    return components;
  }
  @Override
  public String toString() {
    StringBuilder stringBuilder = new StringBuilder();
    for (String pathComponent : components) {
      stringBuilder.append("/" + pathComponent);
    }
    return stringBuilder.toString();
  }
}

MyPathTests.java

import static org.hamcrest.Matchers.is;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.hamcrest.collection.IsEmptyCollection.empty;
import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.junit.Assert.assertThat;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import com.google.common.base.Joiner;
@RunWith(Enclosed.class)
public class MyPathTests {
  public static class GetComponents {
    @Test
    public void componentsCorrespondToFactoryArguments() {
      String[] components = { "Test1", "Test2", "Test3" };
      MyPath myPath = MyPath.ofComponents(components);
      assertThat(myPath.getComponents(), contains(components));
    }
  }
  public static class OfComponents {
    @Test
    public void acceptsArrayOfComponents() {
      MyPath.ofComponents("Test1", "Test2", "Test3");
    }
    @Test
    public void acceptsSingleComponent() {
      MyPath.ofComponents("Test1");
    }
    @Test(expected = IllegalArgumentException.class)
    public void emptyStringVarArgsThrows() {
      MyPath.ofComponents(new String[] { });
    }
    @Test(expected = NullPointerException.class)
    public void nullStringVarArgsThrows() {
      MyPath.ofComponents((String[]) null);
    }
    @Test(expected = IllegalArgumentException.class)
    public void rejectsInterspersedEmptyComponents() {
      MyPath.ofComponents("Test1", "", "Test2");
    }
    @Test(expected = IllegalArgumentException.class)
    public void rejectsSingleEmptyComponent() {
      MyPath.ofComponents("");
    }
    @Test
    public void returnsNotNullValue() {
      assertThat(MyPath.ofComponents("Test"), is(notNullValue()));
    }
  }
  public static class Root {
    @Test
    public void hasComponents() {
      assertThat(MyPath.ROOT.getComponents(), is(not(empty())));
    }
    @Test
    public void hasExactlyOneComponent() {
      assertThat(MyPath.ROOT.getComponents(), hasSize(1));
    }
    @Test
    public void hasExactlyOneInboxComponent() {
      assertThat(MyPath.ROOT.getComponents(), contains("ROOT"));
    }
    @Test
    public void isNotNull() {
      assertThat(MyPath.ROOT, is(notNullValue()));
    }
    @Test
    public void toStringIsSlashSeparatedAbsolutePathToInbox() {
      assertThat(MyPath.ROOT.toString(), is(equalTo("/ROOT")));
    }
  }
  public static class ToString {
    @Test
    public void toStringIsSlashSeparatedPathOfComponents() {
      String[] components = { "Test1", "Test2", "Test3" };
      String expectedPath =
          MyPath.SEPARATOR + Joiner.on(MyPath.SEPARATOR).join(components);
      assertThat(MyPath.ofComponents(components).toString(),
          is(equalTo(expectedPath)));
    }
  }
  @Test
  public void testPathCreationFromComponents() {
    String[] pathComponentArguments = new String[] { "One", "Two", "Three" };
    MyPath myPath = MyPath.ofComponents(pathComponentArguments);
    assertThat(myPath.getComponents(), contains(pathComponentArguments));
  }
}

问题,明确表达

  • 是否有用于构建单元测试的技术列表?比我上面过于简化的列表(例如检查空值,检查边界,检查预期的异常等)更高级的东西,也许可以在一本书或一个URL中访问?

  • 一旦我有了一个采用某种类型参数的方法,我是否可以让任何Eclipse插件为我生成一个用于测试的存根?也许使用Java注释来指定有关该方法的元数据,并让该工具为我实现相关的检查?(例如 @MustBeLowerCase、 @ShouldBeOfSize(n=3)、 ...)

我发现必须记住所有这些“QA技巧”和/或应用它们很乏味和机器人,我发现复制和粘贴很容易出错,我发现当我像上面那样编码时,它不会自我记录。诚然,Hamcrest朝着专门化测试类型的大方向发展(例如,在使用正则表达式的String对象上,在File对象上等),但显然不会自动生成任何测试存根,也不会反映代码及其属性并为我准备工具。

请帮我做得更好。

附言

不要告诉我,我只是在介绍代码,这是一个愚蠢的包装器,围绕着从静态工厂方法中提供的路径步骤列表创建Path的概念,这是一个完全虚构的例子,但它显示了参数验证的“几个”案例......如果我举一个更长的例子,谁会真正阅读这篇文章?


答案 1
  1. 请考虑使用 ExpectedException 而不是 。这是因为,例如,如果您期望 a 并且您的测试在设置中引发此异常(在调用所测试的方法之前),则测试将通过。在调用所测试的方法之前,您将期望放在前面,因此没有机会出现这种情况。此外,还允许您测试异常消息,如果您有两个不同的可能被抛出并且需要检查正确的消息,这将很有帮助。@Test(expected...NullPointerExceptionExpectedExceptionExpectedExceptionIllegalArgumentExceptions

  2. 考虑将测试方法与设置和验证隔离开来,这将简化测试审查和维护。当被测类上的方法作为设置的一部分被调用时尤其如此,这可能会混淆哪个是被测方法。我使用以下格式:

    public void test() {
       //setup
       ...
    
       // test (usually only one line of code in this block)
       ...
    
       //verify
       ...
    }
    
  3. 要看的书籍:Clean CodeJUnit In ActionTest Driven Development by Example

    Clean Code有一个关于测试的优秀部分

  4. 我见过的大多数例子(包括Eclipse自动生成的)在测试标题中都有被测试的方法。这便于审查和维护。例如:。您的示例是我见过的第一个使用按测试方法对方法进行分组的示例,这真的很好。但是,它增加了一些开销,并且不会在封闭的测试类之间共享。testOfComponents_nullCaseEnclosed@Before@After

  5. 我还没有开始使用它,但番石榴有一个测试库:番石榴测试杯。我没有机会玩它,但它似乎有一些很酷的东西。例如:NullPointerTest是引用:

  • 一个测试实用工具,用于验证您的方法是否在其任何参数为 null 时抛出 {@link * NullPointerException} 或 {@link UnsupportedOperationException}。若要使用它,必须首先为类使用的参数类型提供有效的默认 * 值。

评论:我意识到上面的测试只是一个例子,但由于建设性的评论可能会有所帮助,所以你来了。

  1. 在测试中,也测试空列表大小写。另外,使用 .getComponentsIsIterableContainingInOrder

  2. 在 测试 中,调用或验证它是否正确处理了各种非错误情况似乎是有意义的。应该有一个测试,其中没有参数传递给 。我看到这是完成的,但为什么不直接做?需要一个测试,其中一个值是通过:因为这将抛出一个NPE。ofComponentsgetComponentstoStringofComponentsofComponents( new String[]{})ofComponents()nullofComponents("blah", null, "blah2")

  3. 在测试中,如前所述,我建议调用一次并对其进行所有三次验证。此外,所有三个不空,大小和包含。测试中的测试是多余的(尽管它是语言学的),我觉得不值得(恕我直言)。ROOTROOT.getComponentsItIterableContainingInOrderis

  4. 在测试中,我觉得隔离被测方法非常有帮助。我会这样写。请注意,我不使用所测试类中的常量。这是因为恕我直言,对被测类的任何功能更改都会导致测试失败。toStringtoStringIsSlashSeparatedPathOfComponents

    @Test     
    public void toStringIsSlashSeparatedPathOfComponents() {       
       //setup 
       String[] components = { "Test1", "Test2", "Test3" };       
       String expectedPath =  "/" + Joiner.on("/").join(components);   
       MyPath path = MyPath.ofComponents(components)
    
       // test
       String value = path.toStrign();
    
       // verify
       assertThat(value, equalTo(expectedPath));   
    } 
    
  5. Enclosed不会运行任何不在内部类中的单元测试。因此不会运行。testPathCreationFromComponents

最后,使用测试驱动开发。这将确保您的测试因正确原因通过,并将按预期失败。


答案 2

我看到你付出了很多努力来真正测试你的课程。好!:)

我的评论/问题将是:

  • 嘲笑呢?你没有提到任何工具
  • 在我看来,你非常关心细节(我并不是说它们不重要!),而忽略了测试类的业务目的。我想它来自你编码优先的事实(你呢?)。我建议的是更多的TDD / BDD方法,并专注于测试类的业务职责。
  • 不确定这给你什么:“分组在静态嵌套类中的方法测试”?
  • 关于自动生成测试存根等。简单地说:不要。您最终将测试实现而不是行为。

推荐