
junit测试中,由于默认的`per_method`实例生命周期,每个测试方法都会创建新的测试类实例,导致类字段(包括`final`字段)在不同测试方法间重新初始化,而非“重载”。通过使用`@testinstance(testinstance.lifecycle.per_class)`可以强制junit为所有测试方法使用同一个实例,从而避免字段重复初始化,但需警惕这可能破坏测试独立性原则。
JUnit测试中的类实例行为观察
在编写JUnit单元测试时,开发者可能会观察到一个现象:测试类中的实例字段,特别是final修饰的字段,在不同的测试方法执行时会呈现出不同的值,甚至测试类的哈希码也会发生变化。这使得一些开发者误以为测试类在每次测试方法执行时都被“重载”了。
例如,考虑以下测试类:
import org.apache.commons.lang3.RandomStringUtils; // 假设已引入此依赖
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
class SomeTest {
private final String aRandomString = RandomStringUtils.randomAlphabetic(10);
@Test
void a() {
System.out.println("Method a: " + aRandomString + ", Hash: " + this.hashCode());
}
@Test
void b() {
System.out.println("Method b: " + aRandomString + ", Hash: " + this.hashCode());
}
}在执行a()和b()方法时,aRandomString的值很可能会不同,并且this.hashCode()也会显示不同的值。这并非因为类被重载,而是因为JUnit的默认行为导致了不同的测试实例。
JUnit测试实例生命周期解析
JUnit 5(JUnit Jupiter)引入了TestInstance.Lifecycle枚举来管理测试类的实例生命周期。它有两种主要的模式:
-
TestInstance.Lifecycle.PER_METHOD (默认值) 这是JUnit的默认行为。在这种模式下,JUnit会为测试类中的每一个测试方法创建一个全新的测试类实例。这意味着:
- 每个测试方法都会在自己的、独立的测试类实例上执行。
- 测试类中的所有实例字段(包括final字段)都会在每个测试方法执行前重新初始化。
- 这种设计旨在确保测试方法之间的隔离性,避免共享状态导致的测试依赖和不稳定性。
- 上文观察到的aRandomString值变化和hashCode变化正是这种模式的体现。
-
TestInstance.Lifecycle.PER_CLASS 在这种模式下,JUnit会为整个测试类只创建一个测试类实例。这个单一实例将被该测试类中的所有测试方法共享。这意味着:
- 所有测试方法都将在同一个测试类实例上执行。
- 测试类中的实例字段只会在该实例创建时初始化一次,并在所有测试方法之间保持其值。
- 这种模式通常用于需要昂贵设置操作(如数据库连接、Spring上下文加载)只需执行一次的场景,以提高测试效率。
控制测试实例生命周期
要改变JUnit的默认行为,使测试类只创建一个实例,可以使用@TestInstance注解并指定其生命周期为PER_CLASS。
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SomeTestWithPerClass {
private final String aRandomString = RandomStringUtils.randomAlphabetic(10);
@Test
void a() {
System.out.println("Method a: " + aRandomString + ", Hash: " + this.hashCode());
}
@Test
void b() {
System.out.println("Method b: " + aRandomString + ", Hash: " + this.hashCode());
}
}应用@TestInstance(TestInstance.Lifecycle.PER_CLASS)后,aRandomString的值在a()和b()方法中将保持一致,且this.hashCode()也将相同,因为它指向的是同一个实例。
注意事项与最佳实践
虽然PER_CLASS模式可以解决字段重复初始化的问题,但在使用时务必谨慎,因为它可能违反单元测试的核心原则:
- 测试独立性原则 (FIRST原则):单元测试应是独立的、可重复的。PER_CLASS模式下,测试方法共享同一个实例状态,一个测试方法对实例状态的修改可能会影响后续测试方法的执行结果,导致测试失败或结果不可预测。这使得测试变得脆弱,难以维护和调试。
- 副作用管理:如果测试方法会修改共享实例的状态,那么在每个测试方法执行后,需要手动清理或重置状态,以确保下一个测试方法在一个干净的环境中运行。这增加了测试代码的复杂性。
-
适用场景:PER_CLASS模式更适用于以下场景:
- 集成测试或端到端测试:这些测试通常涉及昂贵的资源初始化(如启动Spring Boot应用上下文、连接真实数据库),为每个测试方法都重新初始化一次会非常耗时。在这种情况下,共享一个实例可以显著提高测试速度。
- 只读共享资源:当测试类中的字段是只读的,并且在测试方法之间不会被修改时,使用PER_CLASS是安全的。
- 特殊性能要求:在极少数情况下,为了极致的性能优化,且能严格控制状态副作用时。
总结: JUnit默认的PER_METHOD生命周期是确保单元测试独立性的基石。当观察到测试类字段在不同测试方法间变化时,这通常是JUnit为了隔离测试而创建新实例的正常行为。如果确实需要共享测试实例状态(例如,为了性能优化或管理昂贵资源),可以使用@TestInstance(TestInstance.Lifecycle.PER_CLASS)。然而,在使用此模式时,必须仔细权衡其带来的便利性与可能破坏测试独立性的风险,并确保对共享状态进行妥善管理。对于大多数纯粹的单元测试,保持默认的PER_METHOD行为是更稳健的选择。










