JUnit 中是以测试方法为一个独立的生命周期
在研究 JUnit 5 新特性的时候,学习到其中有一节 Test Instance Lifecycle, 才意识到对 JUnit 的理解一直存在一个误区,以为 JUnit 是以测试类为一个生命周期的,其实不然。不管是 JUnit 5 还是 JUnit 4 或更早的版本,JUnit 都是以测试方法为一个独立的生命周期。
只是到了 JUnit 5 提供了方法来把生命周期由方法改为测试类,对于单个测试类可以使用注解 @TestInstance(Lifecycle.PER_CLASS) 来指定用一个测试实例来跑所有的测试方法,这就意味着测试类中的成员变量只被初始化一次。@TestInstance 的 Lifecycle 默认是 PER_METHOD, JUnit 4 就是 PER_METHOD, 而且是不能改的。如果在 JUnit 5 中改变为 PER_CLASS, 恐怕反而会出许多乱子,每个测试方法本就该是完全独立的。
比如在同一个类中多个测试方法使用了同一个实例变量的情况下,总会用一个 @After 方法来复位该实例变量,现在才知道那是多余的。像下面的代码
1public class CalculatorTest {
2
3 private int number = 100;
4
5 @Test
6 public void test1() {
7 System.out.println(number);
8 number = 200;
9 }
10
11 @Test
12 public void test2() {
13 System.out.println(number);
14 number = 300;
15 }
16}两个方法引用了同一个实例变量,但是那两个测试用例不管怎么执行都是输出 100, 用不着 @After 方法来复位实例变量 number
1@After //这个完全是多余的
2public void tearDown() {
3 number = 100;
4}这就是因为 JUnit 是以测试方法为一个独立的生命周期,JUnit 框架执行上面那个测试方法时相当于作了如下操作:
1new CalculatorTest().test1();
2new CalculatorTest().test2();而不是
1CalculatorTest calculatorTest = new CalculatorTest();
2calculatorTest.test1();
3calculatorTest.test2();那么我们还要 @After 来做什么呢?如果测试方法是串行执行时,可用它来复位操作的外部资源,例如删除临时文件。
既然 JUnit 是以测试方法为生命周期,那么一个类的多个测试方法是可以并发执行的,这时候它们操作同一资源时要多留点心了。
为加深理解测试方法为生命周期的概念,我们通过下面 JUnit 4 的一个例子来验证
1import org.junit.Test;
2
3public class CalculatorTest {
4
5 private int number;
6
7 @Test
8 public void test1() throws InterruptedException {
9 System.out.println("test1 start " + System.currentTimeMillis());
10 Thread.sleep(500);
11 number = 100;
12 System.out.println("test1 end " + System.currentTimeMillis() + " number " + number + ", instance " + this);
13 }
14
15 @Test
16 public void test2() {
17 System.out.println("test2 start " + System.currentTimeMillis());
18 System.out.println("test2 end " + System.currentTimeMillis() + " number " + number + ", instance " + this);
19 }
20}JUnit 4 默认时同一个类中多个方法不会并发执行,可以用 ParallelComputer 开启方法级别的并发执行,这是后话。
我们只说默认情况下,上面的 test1() 总是先执行,然后才 test2()。如果测试方法是一个生命周期,那么 test2() 输出的 number 仍然是默认值 0,而不是 test1() 设置的 100, 看效果
test1 start 1506308523747
test1 end 1506308524249 number 100, instance cc.unmi.CalculatorTest@5d6f64b1
test2 start 1506308524251
test2 end 1506308524251 number 0, instance cc.unmi.CalculatorTest@78e03bb5
没错,尽管 test1 先执行完,设置了 number 为 100, 但 test2 中得到的都是 number 的默认值 0; 因为每次测试方法都是一个新实例,看 this 的实例地址。
之所以有这样一个误区,一部分原因也是被 @BeforeClass 和 @AfterClass 两个注解混淆了视听。
比如 @BeforeClass 很容易令人误以为是在创建好了某个测试用例实例,在执行它任何一个测试方法前唯一执行一次 @BeforeClass 注解的方法。
正确理解 @BeforeClass 和 @AfterClass 应该是,在一次 JUnitCore 运行周期中
@BeforeClass:注解的方法在创建第一个该测试类实例前执行一次
@AfterClass: 注解的方法在执行后该类最后一个测试方法后执行一次
明白了 JUnit 4 是以测试方法为独立的生命周期运转,那么同一个类中多个测试方法是有能力并发执行的。具体做法可参考:Running junit tests in parallel in a Maven build?
还可用 tempus-fugit, 然后测试类用 @RunWith(ConcurrentTestRunner.class)。
用 ParallelComputer 的办法我尝试了以下几种都未能成功
1@Test
2public void test() {
3 Class[] cls={ParallelTest1.class,ParallelTest2.class };
4
5 //Parallel among classes
6 JUnitCore.runClasses(ParallelComputer.classes(), cls);
7
8 //Parallel among methods in a class
9 JUnitCore.runClasses(ParallelComputer.methods(), cls);
10
11 //Parallel all methods in all classes
12 JUnitCore.runClasses(new ParallelComputer(true, true), cls);
13} 实际中应该值得去研究如何通过配置 maven-surefire-plugin 来支持测试方法并发执行。
我倒是用 @RunWith(ConcurrentTestRunner.class) 看到了上面测试类执行的输出效果
test1 start 1506312917860
test2 start 1506312917860
test2 end 1506312917860 number 0, instance cc.unmi.CalculatorTest@5accd8d6
test1 end 1506312918363 number 100, instance cc.unmi.CalculatorTest@5310aea3
参考:
永久链接 https://yanbin.blog/junit-test-instance-lifecycle/, 来自 隔叶黄莺 Yanbin's Blog[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。