自定义JUnit Rules
要编写自定义规则,需要实现TestRule接口。这个接口定义了一个apply(Statement, Description)方法Statement。Statement代表JUnit运行时内的测试,Statement#evaluate()运行这些测试。说明描述了单个测试。它允许通过反射来读取关于测试的信息。
以下是在测试执行之前和之后添加日志语句的一个简单示例:
public class CustomRule implements TestRule { private Description description; @Override public Statement apply(Statement base, Description description) { this.description = description; return new MyStatement(base); } public class MyStatement extends Statement { public final Statement base; public MyStatement(Statement statement) { this.base = statement; } @Override public void evaluate() throws Throwable { System.out.println("CustomRule" + description.getMethodName() + "Started"); try { base.evaluate(); } finally { System.out.println("CustomRule" + description.getMethodName() + "Finished"); } } } } |
要使用这个规则,简单地添加一个注解@Rule到你的测试类的字段。
@Rule public CustomRule customRule = new CustomRule(); |
Categories
Category继承自Suit,Category似乎是Suit的加强版,它和Suit一样提供了将若干测试用例类组织成一组的能力,除此以外它可以对各个测试用例进行分组,使你有机会只选择需要的部分用例。
具体示例如下:
public interface FastTests { } public interface SlowTests { } public class A { @Test public void a() { fail(); } @Category(SlowTests.class) @Test public void b() { } } @Category({ SlowTests.class, FastTests.class }) public class B { @Test public void c() { } } @RunWith(Categories.class) @IncludeCategory(SlowTests.class) @SuiteClasses({ A.class, B.class }) public class SlowTestSuite { // 运行 A.b 和 B.c, 不会运行 A.a } @RunWith(Categories.class) @IncludeCategory(SlowTests.class) @ExcludeCategory(FastTests.class) @SuiteClasses({ A.class, B.class }) public class SlowTestSuite { // 运行 A.b, 不会运行 A.a 和 B.c } |
Theories
Theories继承自BlockJUnit4ClassRunner,提供了除Parameterized之外的另一种参数测试解决方案——似乎更强大。Theories不再需要使用带有参数的Constructor而是接受有参的测试方法,修饰的注解也从@Test变成了@Theory,而参数的提供则变成了使用@DataPoint或者@Datapoints来修饰的变量,两者的唯一不同是前者代表一个数据后者代表一组数据。Theories会尝试所有类型匹配的参数作为测试方法的入参(有点排列组合的意思)。看一个使用Theories的例子:
@RunWith(Theories.class) public class TheoriesTest { @DataPoint public static String nameValue1 = "Tony"; @DataPoint public static String nameValue2 = "Jim"; @DataPoint public static int ageValue1 = 10; @DataPoint public static int ageValue2 = 20; @Theory public void testMethod(String name, int age) { System.out.println(String.format("%s's age is %s", name, age)); } } |
输出结果:
Tony's age is 10
Tony's age is 20
Jim's age is 10
Jim's age is 20
同样使用@DataPoints可以获得一样的效果:
@RunWith(Theories.class) public class TheoriesTest2 { @DataPoints public static String[] names = {"Tony", "Jim"}; @DataPoints public static int[] ageValue1 = {10, 20}; @Theory public void testMethod(String name, int age) { System.out.println(String.format("%s's age is %s", name, age)); } } |
除此以外Theories还可以支持自定义数据提供的方式,需要继承JUnit的ParameterSupplier类。下面的代码使用ParameterSupplier实现上述例子:
public class NameSupplier extends ParameterSupplier { @Override public List<PotentialAssignment> getValueSources(ParameterSignature sig) throws Throwable { PotentialAssignment nameAssignment1 = PotentialAssignment.forValue("name", "Tony"); PotentialAssignment nameAssignment2 = PotentialAssignment.forValue("name", "Jim"); return Arrays.asList(nameAssignment1, nameAssignment2); } } public class AgeSupplier extends ParameterSupplier { @Override public List<PotentialAssignment> getValueSources(ParameterSignature sig) throws Throwable { PotentialAssignment ageAssignment1 = PotentialAssignment.forValue("age", 10); PotentialAssignment ageAssignment2 = PotentialAssignment.forValue("age", 20); return Arrays.asList(ageAssignment1, ageAssignment2); } } @RunWith(Theories.class) public class TheoriesTest3 { @Theory public void testMethod(@ParametersSuppliedBy(NameSupplier.class) String name, @ParametersSuppliedBy(AgeSupplier.class) int age) { System.out.println(String.format("%s's age is %s", name, age)); } } |
Assume
Assume直译为假设,是JUnit提供的一套用于判断测试用例的入参是否有业务含义的工具,如果入参不符合预期时会抛出AssumptionViolatedException,默认的BlockJUnit4ClassRunner及其子类会捕获这个异常并跳过当前测试,如果使用自定义的Runner则无法保证行为,视Runner的实现而定。
Assume提供的验证方法包括: assumeTrue/assumeFalse、 assumeNotNull、 assumeThat、 assumeNoException 。具体含义都比较简单。
假设我们有一个打印姓名和年龄的测试用例,使用Theories提供多套参数测试,由于实际年龄不存在负数所以使用Assume排除不合理的数据:
@RunWith(Theories.class) public class AssumeTest { @DataPoints public static String[] names = {"LiLei", "HanMeiMei"}; @DataPoints public static int[] ages = {10, -2, 12}; @Theory public void printAge(String name, int age) { Assume.assumeTrue(age > 0); System.out.println(String.format("%s's Age is %s.", name, age)); } } 输出结果为: LiLei's Age is 10. LiLei's Age is 12. HanMeiMei's Age is 10. HanMeiMei's Age is 12. |
JUnit生命周期
JUnit提供了@Before、@BeforeClass、@After、@AfterClass注解,这些方法的执行顺序是怎样的?通过下面的代码来演示JUnit的生命周期:
public class JUnitLifeCycleTest { public JUnitLifeCycleTest() { super(); System.out.println("<<Person Constructor>>"); } @BeforeClass public static void beforeClassM() { System.out.println("<<Before Class>>"); } @Before public void beforeM() { System.out.println("<<Before>>"); } @AfterClass public static void afterClassM() { System.out.println("<<After Class>>"); } @After public void after() { System.out.println("<<After>>"); } @Test public void testMethod1() { System.out.println("Test Method 1."); } @Test public void testMethod2() { System.out.println("Test Method 2."); } } 输出结果: <<Before Class>> <<Person Constructor>> <<Before>> Test Method 1. <<After>> <<Person Constructor>> <<Before>> Test Method 2. <<After>> <<After Class>> |
@BeforeClass:修饰static的方法,在整个类执行之前执行该方法一次。比如你的测试用例执行前需要一些高开销的资源(连接数据库)可以用@BeforeClass搞定。值得注意的是如果测试用例类的父类中也存在@BeforeClass修饰的方法,它将在子类的@BeforeClass之前执行。
@AfterClass:同样修饰static的方法,在整个类执行结束前执行一次。如果你用@BeforeClass创建了一些资源现在是时候释放它们了。
@Before:修饰public void的方法,在每个测试用例(方法)执行时都会执行。
@After:修饰public void的方法,在每个测试用例执行结束后执行。
Constructor:每个测试用例都会重新创建当前的Class实例,可以看到Constructor执行了两次。
小结
这篇文字大概简单介绍了JUnit的使用,相对来说是比较简单,也是比较容易理解的,希望能帮助到大家,后续计划再有一篇记录自己在阅读JUnit4源码码的一些收获和感想。
Assert可以帮我们验证一个方法的返回结果。然而,这些只能帮我们测试有返回值的那些方法。一个类的方法分两种,一是有返回值的方法,这些可以通过我们今天讲的JUnit来做测试。而另外一种没有返回值的方法,即void方法,则要通过另外一个框架,Mockito,来验证它的正确性。至于怎么样验证void方法的正确性,以及Mockito的使用,请关注后续文章。