23

意图

我正在寻找以下内容:

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

当前环境

  • Eclipse作为 IDE
  • JUnit作为测试框架,集成到 Eclipse 中
  • Hamcrest作为“匹配器”库,以提高断言的可读性
  • 用于前置条件验证的Google Guava

目前的方法

结构

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

方法

  • 注意null价值观
  • 注意空的List<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();
  }
}

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),...)

我发现必须记住所有这些“质量检查技巧”和/或应用它们很乏味且像机器人一样,我发现复制和粘贴很容易出错,而且当我像我一样编写代码时发现它不是自我记录的以上。诚然,Hamcrest朝着专门测试类型的大方向发展(例如,使用 RegEx对String对象、对File对象等),但显然不会自动生成任何测试存根,也不会反映代码及其属性并准备对我来说是一个安全带。

请帮我把这个做得更好。

附言

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

4

4 回答 4

9
  1. 考虑使用ExpectedException而不是@Test(expected.... 这是因为,例如,如果您期望 aNullPointerException并且您的测试在您的设置中抛出此异常(在调用被测方法之前),您的测试将通过。随着ExpectedException您在调用被测方法之前立即放置期望,因此没有机会。此外,ExpectedException允许您测试异常消息,如果您有两个不同IllegalArgumentExceptions的可能抛出并且您需要检查正确的一个,这将很有帮助。

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

    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是引用:

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

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

  1. 在测试getComponents中,也测试空列表案例。另外,使用IsIterableContainingInOrder.

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

  3. 在测试ROOT中,如前所述,我建议调用ROOT.getComponents一次并对其进行所有三个验证。此外,ItIterableContainingInOrder是否所有三个非空​​、大小和包含。测试is中的 是无关的(尽管它是语言的),我觉得不值得拥有(恕我直言)。

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

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

于 2012-09-20T13:33:50.603 回答
3

I see you put a lot of effort to really test your classes. Good! :)

My comments/questions would be:

  • what about mocking? you do not mention any tool for this
  • it seems to me that you care a lot about the nitty-gritty details (I do not say they are not important!), while neglecting the business purpose of tested class. I guess it comes from the fact you code code-first (do you?). What I would suggest is more TDD/BDD approach and focus on business responsibilities of the tested class.
  • not sure what this gives you: "Method testing grouped in static nested classes"?
  • regarding auto-generation of test stubs etc. Simply put: don't. You will end up testing implementation instead of behaviour.
于 2012-09-20T19:02:10.133 回答
2

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

是否有用于构建单元测试的技术列表?

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

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

  • 确保涵盖通过您的方法的所有路径。
  • 确保所有重要功能都被多个测试覆盖,为此我经常使用 Parameterized。

我发现真正有用的一件事是询问这个方法该做什么,而不是这个方法做什么。这样,您可以以更开放的心态编写测试。

我觉得有用的另一件事是减少与测试相关的样板,这样我就可以更轻松地阅读测试。添加测试越容易越好。我发现 Parameterized 非常适合这个。对我来说,测试的可读性是关键。

因此,以上面的示例为例,如果我们放弃“仅在方法中测试一件事”的要求,我们会得到

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"} ...

于 2012-09-20T13:50:19.097 回答
0

Well, I will post 2 difference answers.

  1. As James Coplien stated unit test is worthless. I disagree with him on this issue, but may be you will find this helpful to consider to unit test less instead of searching for automatic solution.

  2. Consider to use Theories with DataPoints. I think this will minimize your problem significantly. Also, using mock can help you.

于 2012-09-23T17:48:04.643 回答