在研究 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
方法来复位该实例变量,现在才知道那是多余的。像下面的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class CalculatorTest { private int number = 100; @Test public void test1() { System.out.println(number); number = 200; } @Test public void test2() { System.out.println(number); number = 300; } } |
两个方法引用了同一个实例变量,但是那两个测试用例不管怎么执行都是输出 100, 用不着 @After 方法来复位实例变量 number
1 2 3 4 |
@After //这个完全是多余的 public void tearDown() { number = 100; } |
这就是因为 JUnit 是以测试方法为一个独立的生命周期,JUnit 框架执行上面那个测试方法时相当于作了如下操作:
1 2 |
new CalculatorTest().test1(); new CalculatorTest().test2(); |
而不是
1 2 3 |
CalculatorTest calculatorTest = new CalculatorTest(); calculatorTest.test1(); calculatorTest.test2(); |
那么我们还要 @After
来做什么呢?如果测试方法是串行执行时,可用它来复位操作的外部资源,例如删除临时文件。
既然 JUnit 是以测试方法为生命周期,那么一个类的多个测试方法是可以并发执行的,这时候它们操作同一资源时要多留点心了。
为加深理解测试方法为生命周期的概念,我们通过下面 JUnit 4 的一个例子来验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import org.junit.Test; public class CalculatorTest { private int number; @Test public void test1() throws InterruptedException { System.out.println("test1 start " + System.currentTimeMillis()); Thread.sleep(500); number = 100; System.out.println("test1 end " + System.currentTimeMillis() + " number " + number + ", instance " + this); } @Test public void test2() { System.out.println("test2 start " + System.currentTimeMillis()); System.out.println("test2 end " + System.currentTimeMillis() + " number " + number + ", instance " + this); } } |
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 2 3 4 5 6 7 8 9 10 11 12 13 |
@Test public void test() { Class[] cls={ParallelTest1.class,ParallelTest2.class }; //Parallel among classes JUnitCore.runClasses(ParallelComputer.classes(), cls); //Parallel among methods in a class JUnitCore.runClasses(ParallelComputer.methods(), cls); //Parallel all methods in all classes JUnitCore.runClasses(new ParallelComputer(true, true), cls); } |
实际中应该值得去研究如何通过配置 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 Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。