可靠的Javaunit testing自动化? (JUnit的/ Hamcrest / …)

意图

我正在寻找以下内容:

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

目前的环境

  • Eclipse作为IDE
  • JUnit作为测试框架,集成到Eclipse中
  • Hamcrest作为“匹配器”库,具有更好的断言可读性
  • Google Guava用于前置条件validation

目前的做法

结构体

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

方法

  • 注意null
  • 注意空列表
  • 注意空字符串
  • 注意空数组
  • 注意由代码更改的对象状态不变量(后置条件)
  • 方法接受记录的参数类型
  • 边界检查(例如Integer.MAX_VALUE等…)
  • 通过特定类型记录不变性(例如Google Guava ImmutableList
  • ……有这个列表吗? 好的测试列表示例:
    • 在数据库项目中检查的事项(例如CRUD,连接,日志记录,……)
    • 要检查multithreading代码的事情
    • 要检查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 components; private MyPath(String[] components) { this.components = ImmutableList.copyOf(components); } public ImmutableList 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)); } } 

问题,明确表达

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

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

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

请帮我把它做得更好。

PS

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

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

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

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

    清洁代码有一个很好的测试部分

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

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

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

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

  1. 在测试getComponentsgetComponents测试空列表大小写。 另外,使用IsIterableContainingInOrder

  2. 在测试ofComponents ,似乎有必要调用getComponentstoString来validation它是否正确处理了各种非错误情况。 应该有一个测试,其中没有参数传递给ofComponents 。 我看到这是通过ofComponents( new String[]{})但为什么不只是ofComponents()呢? 需要一个测试,其中null是传递的值之一: ofComponents("blah", null, "blah2")因为这将抛出一个NPE。

  3. 在测试ROOT ,如前所述,我建议一次调用ROOT.getComponents并对其进行所有三次validation。 此外, ItIterableContainingInOrder执行所有三个非空​​,大小和包含。 在测试中是多余的(虽然它是语言学的),我觉得不值得拥有(恕我直言)。

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

     @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不会运行任何不属于内部类的unit testing。 因此,不会运行testPathCreationFromComponents

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

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

我的意见/问题是:

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

好的,这是我对你的问题的看法:

是否有用于构建unit testing的技术列表?

简短的回答,没有。 您的问题是,要为方法生成测试,您需要分析它的作用并为每个位置的每个可能值进行测试。 有测试生成器,但是IIRC,他们没有生成可维护的代码(请参阅测试驱动开发的参考资料 )。

你已经有了一个相当不错的清单,我要补充一下:

  • 确保涵盖通过方法的所有路径。
  • 确保所有重要function都由多个测试覆盖,我为此使用了参数化。

我觉得有用的一件事就是问这个方法应该做什么,而不是这个方法做什么。 这样,您就可以更开放的态度编写测试。

我觉得有用的另一件事是减少与测试相关的样板,这样我就可以更容易地阅读测试。 添加测试越容易,越好。 我觉得参数化非常好。 对我来说,测试的可读性是关键。

所以,以上面的例子为例,如果我们放弃了“只在方法中测试一件事”的要求,我们就会得到

 public static class Root { @Test public void testROOT() { assertThat("hasComponents", MyPath.ROOT.getComponents(), is(not(empty()))); assertThat("hasExactlyOneComponent", MyPath.ROOT.getComponents(), hasSize(1)); assertThat("hasExactlyOneInboxComponent", MyPath.ROOT.getComponents(), contains("ROOT")); assertThat("isNotNull", MyPath.ROOT, is(notNullValue())); assertThat("toStringIsSlashSeparatedAbsolutePathToInbox", MyPath.ROOT.toString(), is(equalTo("/ROOT"))); } } 

我做了两件事,我已经将描述添加到断言中,并且我已将所有测试合并为一个。 现在,我们可以阅读测试,看看我们实际上已经有了重复测试。 我们可能不需要测试is(not(empty()) && is(notNullValue())等。这违反了每个方法规则的一个断言,但我认为这是合理的,因为你删除了大量的样板而没有切割覆盖范围。

我可以自动执行检查吗?

是。 但我不会使用注释来做到这一点。 假设我们有一个类似的方法:

 public boolean validate(Foobar foobar) { return !foobar.getBar().length > 40; } 

所以我有一个测试方法,如下所示:

 private Foobar getFoobar(int length) { Foobar foobar = new Foobar(); foobar.setBar(StringUtils.rightPad("", length, "x")); // make string of length characters return foobar; } @Test public void testFoobar() { assertEquals(true, getFoobar(39)); assertEquals(true, getFoobar(40)); assertEquals(false, getFoobar(41)); } 

上述方法很容易根据长度进行分解,当然也可以进入参数化测试。 故事的道德,你可以像使用非测试代码一样分解你的测试。

所以为了回答你的问题,根据我的经验,我得出结论,你可以通过在测试中减少样板,通过使用你的参数化和分解的明智组合来帮助完成所有组合。试验。 作为最后一个例子,我将使用Parameterized实现您的测试:

@RunWith(Parameterized.class)public static class OfComponents {@Parameters public static Collection data(){return Arrays.asList(new Object [] [] {{new String [] {“Test1”,“Test2”,“Test3” },null},{new String [] {“Test1”},null},{null,NullPointerException.class},{new String [] {“Test1”,“”,“Test2”},IllegalArgumentException},}) ; }

 private String[] components; @Rule public TestRule expectedExceptionRule = ExpectedException.none(); public OfComponents(String[] components, Exception expectedException) { this.components = components; if (expectedException != null) { expectedExceptionRule.expect(expectedException); } } @Test public void test() { MyPath.ofComponents(components); } 

请注意,以上内容未经过测试,可能无法编译。 从上面,您可以将数据分析为输入并添加(或至少考虑添加)所有内容的组合。 例如,你没有测试{“Test1”,null,“Test2”} …

好吧,我会发布2个不同的答案。

  1. 正如James Coplien所说,unit testing毫无价值。 我在这个问题上不同意他,但你可能会发现这有助于考虑减少unit testing,而不是寻找自动解决方案。

  2. 考虑将理论与DataPoints一起使用。 我认为这会大大减少您的问题。 另外,使用mock可以帮助你。