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

Rob*_*ino 23 java eclipse junit unit-testing

意图

我正在寻找以下内容:

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

目前的环境

目前的做法

结构体

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

方法

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

示例代码

这是一个人为的例子来展示一些技巧.


MyPath.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();
  }
}
Run Code Online (Sandbox Code Playgroud)

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));
  }
}
Run Code Online (Sandbox Code Playgroud)

问题,明确表达

  • 是否有用于构建单元测试的技术列表?比我上面的过度简化列表更先进的东西(例如检查空值,检查边界,检查预期的异常等)可能在书中购买或访问URL?

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

我发现它需要记住所有这些"质量检查技巧"和/或应用它们一样乏味和机器人一样,我发现它很容易复制和粘贴,而且当我像我一样编写代码时,我发现它不能自我记录以上.不可否认,Hamcrest 专注于测试类型的一般方向(例如,使用RegEx,在File对象上使用String对象等),但显然不会自动生成任何测试存根,也不会反映代码及其属性并准备我的安全带.

请帮我把它做得更好.

PS

不要告诉我,我只是提供代码,这是一个愚蠢的包装,围绕从静态工厂方法提供的路径步骤列表中创建路径的概念,这是一个完全组成的例子,但它显示"少数"论证验证的案例......如果我包含更长的例子,谁会真正阅读这篇文章?

Joh*_*n B 9

  1. 考虑使用ExpectedException而不是@Test(expected....这是因为,例如,如果您期望a NullPointerException并且您的测试在您的设置中抛出此异常(在调用测试方法之前),您的测试将通过.随着ExpectedException你在调用被测方法之前就把期望放在一边,所以没有机会这样做.此外,ExpectedException允许您测试异常消息,如果您有两个IllegalArgumentExceptions可能被抛出的异常消息并且需要检查正确的异常消息.

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

    public void test() {
       //setup
       ...
    
       // test (usually only one line of code in this block)
       ...
    
       //verify
       ...
    }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 要看的书:清洁代码,JUnit在行动,测试驱动开发示例

    Clean Code有一个很好的测试部分

  4. 我见过的大多数示例(包括Eclipse自动生成的)在测试标题中都有测试方法.这有助于审查和维护.例如:testOfComponents_nullCase.你的例子是我见过的第一个Enclosed通过测试方法使用to group方法,这非常好.但是,它会增加一些开销,@Before并且@After不会在封闭的测试类之间共享.

  5. 我还没有开始使用它,但是Guava有一个测试库:guava-testlib.我没有机会玩它,但似乎有一些很酷的东西.例如:NullPointerTest是引用:

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

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

  1. 在测试中getComponents,也测试空列表情况.另外,使用IsIterableContainingInOrder.

  2. 在测试中ofComponents,似乎调用getComponentstoString验证它是否正确处理了各种非错误情况是有意义的.应该有一个没有传递参数的测试ofComponents.我知道这已经完成,ofComponents( new String[]{})但为什么不ofComponents()呢?需要测试哪里null是传递的值之一:ofComponents("blah", null, "blah2")因为这将抛出NPE.

  3. 在测试中ROOT,正如之前所指出的,我建议调用ROOT.getComponents一次并对其进行所有三次验证.此外,ItIterableContainingInOrder所有三个都不是空的,大小和包含.将is在测试是extraineous(虽然它是语言),我觉得是不值得拥有(恕我直言)

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

    @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));   
    } 
    
    Run Code Online (Sandbox Code Playgroud)
  5. Enclosed不会运行任何不在内部类中的单元测试.因此testPathCreationFromComponents不会运行.

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