坚实的Java单元测试自动化? (JUnit的/ Hamcrest / ...) [英] Solid Java unit test automation? (JUnit/Hamcrest/...)

查看:209
本文介绍了坚实的Java单元测试自动化? (JUnit的/ Hamcrest / ...)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

意图



我正在寻找以下内容:




  • 单位测试方法


    1. 我的方法是什么?

    2. 我做错了什么

    3. 我在做什么是不必要的?


  • 自动获取尽可能多的费用。



当前环境





当前方法



结构




  • 每类测试一个测试类

  • 以静态嵌套类分组的方法测试

  • 测试方法命名以指定行为测试+预期结果

  • Java指定的预期异常 指定 ,而不是方法名称



方法




  • 注意 null

  • 注意空白 列表< E>

  • 注意空白 String

  • 注意空数组

  • 注意对象状态不变量由代码改变(post-conditions)

  • 方法接受已记录的参数类型

  • 边界检查(例如 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());
返回新的MyPath(组件);
}
private final ImmutableList< String>组件;
private MyPath(String [] components){
this.components = ImmutableList.copyOf(components);
}
public ImmutableList< String> getComponents(){
返回组件;
}
@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 acceptableArrayOfComponents(){
MyPath.ofComponents(Test1,Test2 ,Test3);
}
@Test
public void supportsSingleComponent(){
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 (不是空的())));
}
@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(),
是(equalTo(expectedPath)));
}
}
@Test
public void testPathCreationFromComponents(){
String [] pathComponentArguments = new String [] {One,Two,Three };
MyPath myPath = MyPath.ofComponents(pathComponentArguments);
assertThat(myPath.getComponents(),contains(pathComponentArguments));
}
}






问题,明确表达




  • 有没有一些技术用于构建单元测试?比我上面简单的列表更先进(例如,检查null,检查边界,检查预期的异常等)可能在一本书中可以购买或访问一个URL?


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




我觉得很乏味,机器人像是要记住所有这些QA技巧和/或应用它们,我发现它很容易复制和粘贴,我发现它不是自我记录,当我编写的东西,如上所述。诚然,Hamcrest 图书馆在专业化测试类型的大体方向(例如 String 对象,在 文件 对象等),但显然不会自动生成任何测试存根并不反映代码及其属性,并为我准备一个线束。



请帮助我做得更好。



PS



不要告诉我,我只是提供代码,这是一个愚蠢的包装,围绕创建一个路径的概念一个静态工厂方法提供的路径步骤列表请看这是一个完整的例子,但它显示了一个少数的参数验证情况...如果我包含了一个更长的例子,谁会真的读这个帖子? / em>

解决方案


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


  2. 考虑将测试中的方法与设置和验证隔离,这将有助于测试审查和维护。当被测试类上的方法被调用作为安装程序的一部分时,这是特别真实的,这可能会混淆被测方法。我使用以下格式:

      public void test(){
    // setup
    ...

    // test(通常这个块中只有一行代码)
    ...

    //验证
    ...
    }


  3. 要查看的图书:清洁代码 JUnit In Action 以示例的测试驱动开发



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


  4. 我已经看到(包括什么Eclipse自动生成)测试题目的测试。这有助于审查和维护。例如: testOfComponents_nullCase 。您的例子是我看到的第一个使用 Enclosed 通过测试方法对方法进行分组,这是非常好的。但是,由于 @Before @After 不会在封闭的测试类之间共享,所以添加了一些开销。 >


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






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


评论:我认识到上面的测试只是一个例子,但由于建设性的评论可能是有帮助的在这里你去。


  1. 在测试 getComponents 列表案例以及。另外,使用 IsIterableContainingInOrder


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


  3. 在测试 ROOT 之前,如前所述,建议调用 ROOT.getComponents 一次并对其进行所有三个验证。另外, ItIterableContainingInOrder 所有三个不为空,大小和包含。测试中的是非常昂贵的(虽然它是语言的),我觉得不值得(IMHO)。


  4. 在测试 toString 时,我觉得隔离测试方法是非常有帮助的。我会写下 toStringIsSlashSeparatedPathOfComponents ,如下所示。请注意,我不使用被测课程的常数。这是因为IM HO IM to to to to b should should cause cause。。。。。。。。。。。。b b b b b b b b b b b b b b b b b b b 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();

    //验证
    assertThat(value,equalTo(expectedPath));
    }


  5. 封闭不会运行任何不在内部类的单元测试。因此 testPathCreationFromComponents 将不会运行。


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


Intent

I am looking for the following:

  • A solid unit testing methodology

    1. What am I missing from my approach?
    2. What am I doing wrong?
    3. What am I doing which is unnecessary?

  • A way to get as much as possible done automatically

Current environment

  • Eclipse as IDE
  • JUnit as a testing framework, integrated into Eclipse
  • Hamcrest as a "matchers" library, for better assertion readability
  • Google Guava for precondition validation

Current approach

Structure

  • One test class per class to test
  • Method testing grouped in static nested classes
  • Test method naming to specify behaviour tested + expected result
  • Expected exceptions specified by Java Annotation, not in method name

Methodology

  • Watch out for null values
  • Watch out for empty List<E>
  • Watch out for empty String
  • Watch out for empty arrays
  • Watch out for object state invariants altered by code (post-conditions)
  • Methods accept documented parameter types
  • Boundary checks (e.g. Integer.MAX_VALUE, etc...)
  • Documenting immutability through specific types (e.g. Google Guava ImmutableList<E>)
  • ... is there a list for this? Examples of nice-to-have testing lists:
    • Things to check in database projects (e.g. CRUD, connectivity, logging, ...)
    • Things to check in multithreaded code
    • Things to check for EJBs
    • ... ?

Sample code

This is a contrived example to show some techniques.


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));
  }
}


Question, phrased explicitly

  • Is there a list of techniques to use to build a unit test? Something much more advanced than my oversimplified list above (e.g. check nulls, check boundaries, check expected exceptions, etc.) perhaps available in a book to buy or a URL to visit?

  • Once I have a method that takes a certain type of parameters, can I get any Eclipse plug-in to generate a stub for my tests for me? Perhaps using a Java Annotation to specify metadata about the method and having the tool materialise the associated checks for me? (e.g. @MustBeLowerCase, @ShouldBeOfSize(n=3), ...)

I find it tedious and robot-like to have to remember all of these "QA tricks" and/or apply them, I find it error-prone to copy and paste and I find it not self-documenting when I code things as I do above. Admittedly, Hamcrest libraries go in the general direction of specialising types of tests (e.g. on String objects using RegEx, on File objects, etc) but obviously do not auto-generate any test stubs and do not reflect on the code and its properties and prepare a harness for me.

Help me make this better, please.

PS

Do not tell me that I am just presenting code which is a silly wrapper around the concept of creating a Path from a list of path steps supplied in a static factory method please, this is a totally made-up example but it shows a "few" cases of argument validation... If I included a much longer example, who would really read this post?

解决方案

  1. Consider using ExpectedException instead of @Test(expected.... This is because if for example you expect a NullPointerException and your test throws this exception in your setup (before calling the method under test) your test will pass. With ExpectedException you put the expect immediately before the call to the method under test so there is no chance of this. Also, ExpectedException allows you to test the exception message which is helpful if you have two different IllegalArgumentExceptions that might be thrown and you need to check for the correct one.

  2. Consider isolating your method under test from the setup and verify, this will ease in test review and maintenance. This is especially true when methods on the class under test are invoked as part of setup which can confuse which is the method under test. I use the following format:

    public void test() {
       //setup
       ...
    
       // test (usually only one line of code in this block)
       ...
    
       //verify
       ...
    }
    

  3. Books to look at: Clean Code, JUnit In Action, Test Driven Development By Example

    Clean Code has an excellent section on testing

  4. Most example I have seen (including what Eclipse autogenerates) have the method under test in the title of the test. This facilitates review and maintenance. For example: testOfComponents_nullCase. Your example is the first I have seen that uses the Enclosed to group methods by method under test, which is really nice. However, it adds some overhead as @Before and @After do not get shared between enclosed test classes.

  5. I have not started using it, but Guava has a test library: guava-testlib. I have not had a chance to play with it but it seems to have some cool stuff. For example: NullPointerTest is quote:

  • A test utility that verifies that your methods throw {@link * NullPointerException} or {@link UnsupportedOperationException} whenever any * of their parameters are null. To use it, you must first provide valid default * values for the parameter types used by the class.

Review: I realize the test above was just an example but since a constructive review might be helpful, here you go.

  1. In testing getComponents, test the empty list case as well. Also, use IsIterableContainingInOrder.

  2. In testing of ofComponents, it seems that it would make sense to call getComponents or toString to validate that it properly handled the various non-error cases. There should be a test where no argument is passed to ofComponents. I see that this is done with ofComponents( new String[]{}) but why not just do ofComponents()? Need a test where null is one of the values passed: ofComponents("blah", null, "blah2") since this will throw an NPE.

  3. In testing ROOT, as has been pointed out before, I suggest calling ROOT.getComponents once and doing all three verifications on it. Also, ItIterableContainingInOrder does all three of not empty, size and contains. The is in the tests is extraineous (although it is linguistic) and I feel is not worth having (IMHO).

  4. In testing toString, I feel it is very helpful to isolate the method under test. I would have written toStringIsSlashSeparatedPathOfComponents as follows. Notice that I do not use the constant from the class under test. This is because IMHO, ANY functional change to the class under test should cause the test to fail.

    @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 will not run any unit test that is not in an inner class. Therefore testPathCreationFromComponents would not be run.

Finally, use Test Driven Development. This will ensure that your tests are passing for the right reason and will fail as expected.

这篇关于坚实的Java单元测试自动化? (JUnit的/ Hamcrest / ...)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆