可靠的 Java 单元测试自动化?(JUnit/Hamcrest/...)
意图
我正在寻找以下内容:
- 可靠的单元测试方法
- 我的方法中遗漏了什么?
- 我做错了什么?
- 我在做什么是不必要的?
- 一种尽可能自动完成的方法
当前环境
目前的做法
结构
- 每个要测试的类一个测试类
- 分组在静态嵌套类中的方法测试
- 测试方法命名,用于指定测试的行为 + 预期结果
- 由 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的概念,这是一个完全虚构的例子,但它显示了参数验证的“几个”案例......如果我举一个更长的例子,谁会真正阅读这篇文章?