[译]从JUnit 4 迁移到JUnit 5 - 权威指南
文章目录
本文翻译自Migrating From JUnit 4 to JUnit 5: A Definitive Guide。

在教程中,我们将了解从JUnit 4迁移到JUnit 5所需的步骤,我们将了解如何将现有测试与新版本一起运行,以及必须进行哪些更改才能迁移代码。
本文是JUnit 5 教程的一部分。
概览
JUnit 5不同于之前的版本,它采用模块化设计,这种新架构的关键点是将编写测试、扩展和工具之间的关注点分开。
JUnit被拆分为3个不同的子工程:
- JUnit Platform是基础,提供构建插件和用于编写测试引擎的API
- JUnit Jupiter是在
JUnit 5中用于编写测试和扩展的新式API - JUnit Vintage允许我们在
JUnit 5中运行JUnit 4编写的测试
以下是JUnit 5相对于JUnit 4的一些优点:
JUnit 4最大的一个缺陷是不支持运行多个Runners(例如不能同时使用SpringJUnit4ClassRunner和Parameterized),而在JUnit 5中可通过注册多个扩展实现。
此外,JUnit 5利用了Java 8中的一些特性如lambda用于惰性评估,JUnit 4从没使用超过Java 7的版本因而错失了Java 8的特性。
同样,JUnit 4在参数化测试方面有缺陷并且缺乏嵌套测试,由此激发了第三方开发者为这些场景开发专门的Runner。
JUnit 5给参数化测试提供了更好的支持,在对嵌套测试提供原生支持的同时也增加了一些新功能。
关键迁移步骤
JUnit在JUnit Vintage测试引擎的帮助下提供了渐进的迁移路径,我们可使用JUnit Vintage测试引擎在JUnit 5中运行JUnit 4相关的测试。
所有JUnit 4相关的类都位于org.junit包下,所有JUnit 5相关的类都位于org.junit.jupiter包下,如果JUnit 4和JUnit 5在当前类路径下同时存在,也不会产生冲突。
因此,我们可以将之前实现的JUnit 4测试与JUnit 5测试保留在一起,直到完成迁移,同时可逐步规划迁移。
下属表格总结了从JUnit 4迁移到JUnit 5中的一些关键步骤:
| 步骤 | 解释 |
|---|---|
| 替换依赖 | JUnit 4使用单个依赖,JUnit 5对迁移支持和JUnit Vintage引擎有额外的依赖项 |
| 替换注解 | JUnit 5中的一些注解与JUnit 4中的相同,一些新注解替换了旧的,并且功能略有不同。 |
| 替换测试类和方法 | 断言和假设已被移动到新的类中,方法参数的顺序在某些场景下所有不同。 |
| 替换runners、规则、扩展 | JUnit 5用一个扩展模型替换了runners和规则,此步骤相较于其它步骤更耗时。 |
接下来我们将更深入地研究每个步骤。
依赖
先看看需要做什么才能在新平台上运行现有测试,为了同时运行JUnit 4和JUnit 5测试,需要如下操作:
JUnit Jupiter用于编写和运行JUnit 5测试JUnit Vintage测试引擎用于运行JUnit 4测试
除此之外,要使用Maven运行测试,我们还需要Surefire插件,需要在pom.xml中添加所需的全部依赖:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>
</dependencies>
相似的,要使用Gradle运行测试的话,同样需要在测试中开启JUnit Platform支持,同时在build.gradle中添加所需的全部依赖:
test {
useJUnitPlatform()
}
dependencies {
testImplementation('org.junit.jupiter:junit-jupiter:5.8.0')
testRuntime('org.junit.vintage:junit-vintage-engine:5.8.0')
}
注解
注解位于org.junit.jupiter.api包中而不是org.junit。
大部分的注解名称也不相同:
| JUnit 4 | JUnit 5 |
|---|---|
@Test |
@Test |
@Before |
@BeforeEach |
@After |
@AfterEach |
@BeforeClass |
@BeforeAll |
@AfterClass |
@AfterAll |
@Ignore |
@Disable |
@Category |
@Tag |
在大多数情况下,我们可以查找并替换相关的包名与类名。
但是,@Test注解不再具有expected和timeout属性。
异常
我们无法在@Test注解中使用expected属性。
可用JUnit 5中的assertThrows()方法来替换JUnit 4中的expected属性:
public class JUnit4ExceptionTest {
@Test(expected = IllegalArgumentException.class)
public void shouldThrowAnException() {
throw new IllegalArgumentException();
}
}
class JUnit5ExceptionTest {
@Test
void shouldThrowAnException() {
Assertions.assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException();
});
}
}
超时
同样也无法在@Test注解中使用timeout属性。
可在JUnit 5中使用assertTimeout()方法来替换JUnit 4中的timeout属性。
public class JUnit4TimeoutTest {
@Test(timeout = 1)
public void shouldTimeout() throws InterruptedException {
Thread.sleep(5);
}
}
class JUnit5TimeoutTest {
@Test
void shouldTimeout() {
Assertions.assertTimeout(Duration.ofMillis(1), () -> Thread.sleep(5));
}
}
测试类和方法
如前所述,断言和假设被移动到新的类中,同样的,方法参数的顺序在某些场景下所有不同。
下述表格总结了JUnit 4和JUnit 5中测试类和测试方法的一些主要的不同点:
| JUnit 4 | JUnit 5 | |
|---|---|---|
| 测试包 | org.junit |
org.junit.jupiter.api |
| 断言类 | Assert |
Assertions |
assertThat() |
MatcherAssert.assertThat() |
|
| 可选的断言消息 | 第一个方法参数 | 最后一个方法参数 |
| 假设类 | Assume |
Assumptions |
assumeNotNull() |
移除 | |
assumeNoException() |
移除 |
值得注意的是,我们在JUnit 4中自己编写的测试类和方法必须是public修饰的。
JUnit 5移除了上述限制,测试方法和测试类可以在包中私有,我们可在提供的示例中看到这种差异。
接下来我们详细看看测试类和测试方法中的变化。
断言
断言方法位于org.junit.jupiter.api.Assertions类中,而不是org.junit.Assert类中。
在大多数场景下,我们可直接查找并替换包名。
然而,如果我们给断言提供了自定义消息,在编译时会报错,可选的断言消息现在是最后一个参数,这种参数顺序看起来更自然:
public class JUnit4AssertionTest {
@Test
public void shouldFailWithMessage() {
Assert.assertEquals("numbers " + 1 + " and " + 2 + " are not equal", 1, 2);
}
}
class JUnit5AssertionTest {
@Test
void shouldFailWithMessage() {
Assertions.assertEquals(1, 2, () -> "numbers " + 1 + " and " + 2 + " are not equal");
}
}
也可以像示例中那样惰性评估断言消息,从而避免构造不必要的复杂消息。
注意
当断言一个String对象并有自定义消息时,由于所有的参数类型都是String我们不会看见编译错误,然而我们可以很容易地发现这些情况,因为当运行它们时测试将会失败。
此外,有些遗留的测试代码会基于JUnit 4中的Assert.assertThat()来使用Hamcrest断言,JUnit 5没有像JUnit 4那样提供Assert.assertThat(),相反,我们必须从Hamcrest的MatcherAssert中导入相关方法:
public class JUnit4HamcrestTest {
@Test
public void numbersNotEqual() {
Assert.assertThat("numbers 1 and 2 are not equal", 1, is(not(equalTo(2))));
}
}
class JUnit5HamcrestTest {
@Test
void numbersNotEqual() {
MatcherAssert.assertThat("numbers 1 and 2 are not equal", 1, is(not(equalTo(2))));
}
}
假设
假设方法位于org.junit.jupiter.Assumptions类中,而不是org.junit.Assume类中。
这些方法有类似的改动,假设消息现在是最后一个参数。
@Test
public class JUnit4AssumptionTest {
public void shouldOnlyRunInDevelopmentEnvironment() {
Assume.assumeTrue("Aborting: not on developer workstation",
"DEV".equals(System.getenv("ENV")));
}
}
class JUnit5AssumptionTest {
@Test
void shouldOnlyRunInDevelopmentEnvironment() {
Assumptions.assumeTrue("DEV".equals(System.getenv("ENV")),
() -> "Aborting: not on developer workstation");
}
}
同样需要注意的是,现在不再有Assume.assumeNotNUll()和Assume.assumeNoException() 。
分类
JUnit 4中的@Category注解在JUnit 5中被@Tag注解替换,此外我们不再使用标记接口,而是向注解传递字符串参数。
在JUnit 4可通过标记接口来使用分类:
public interface IntegrationTest {}
@Category(IntegrationTest.class)
public class JUnit4CategoryTest {}
之后可通过在pom.xml中配置基于标签过滤测试。
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<groups>com.example.AcceptanceTest</groups>
<excludedGroups>com.example.IntegrationTest</excludedGroups>
</configuration>
</plugin>
或者,在使用Gradle时在build.gradle中进行配置:
test {
useJUnit {
includeCategories 'com.example.AcceptanceTest'
excludeCategories 'com.example.IntegrationTest'
}
}
但是在JUnit 5中可直接使用标签来实现:
@Tag("integration")
class JUnit5TagTest {}
Maven中的pom.xml配置也更简单:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<groups>acceptance</groups>
<excludedGroups>integration</excludedGroups>
</configuration>
</plugin>
相似的,build.gradle中的配置也更简洁:
test {
useJUnitPlatform {
includeTags 'acceptance'
excludeTags 'integration'
}
}
Runners
JUnit 4中的@RunWith注解在JUnit 5中不存在,可通过使用org.junit.jupiter.api.extension包和@ExtendWith注解中的新扩展模型实现相似的功能。
Spring Runner
JUnit 4中使用的一个流行的runner是Spring test runner,在JUnit 5中需要将其替换为一个Spring扩展。
如果我们使用Spring 5,该扩展会与Spring Test绑定在一起。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringTestConfiguration.class)
public class JUnit4SpringTest {
}
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SpringTestConfiguration.class)
class JUnit5SpringTest {
}
然而在使用Spring 4时该扩展没有与SpringExtension绑定,我们仍然可以使用它,但需要JitPack仓库中的额外依赖。
要在JUnit 4中使用SpringExtension注解我们需要在pom.xml中添加如下依赖:
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.sbrannen</groupId>
<artifactId>spring-test-junit5</artifactId>
<version>1.5.0</version>
<scope>test</scope>
</dependency>
</dependencies>
类似的,在使用Gradle时也需要在build.gradle中添加相应依赖:
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
dependencies {
testImplementation('com.github.sbrannen:spring-test-junit5:1.5.0')
}
Mockito Runner
JUnit 4中另一个流行的runner是Mockito runner,在使用JUnit 5时需要将其替换为JUnit 5中的Mockito扩展。
要使用Mockito扩展,需要在pom.xml中添加相关的依赖:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>3.6.28</version>
<scope>test</scope>
</dependency>
类似的,使用Gradle时需要在build.gradle中添加依赖:
dependencies {
testImplementation('org.mockito:mockito-junit-jupiter:3.12.4')
}
现在,可将MockitoJUnitRunner简单的替换为MockitoExtension:
@RunWith(MockitoJUnitRunner.class)
public class JUnit4MockitoTest {
@InjectMocks
private Example example;
@Mock
private Dependency dependency;
@Test
public void shouldInjectMocks() {
example.doSomething();
verify(dependency).doSomethingElse();
}
}
@ExtendWith(MockitoExtension.class)
class JUnit5MockitoTest {
@InjectMocks
private Example example;
@Mock
private Dependency dependency;
@Test
void shouldInjectMocks() {
example.doSomething();
verify(dependency).doSomethingElse();
}
}
规则
JUnit 4中的@Rule和@ClassRule注解在JUnit 5中不存在,可通过使用org.junit.jupiter.api.extension包和@ExtendWith注解中的新扩展模型实现相似的功能。
然而,要实现一个平滑的迁移,junit-jupiter-migrationsupport模块提供了JUnit 4中的规则子集和子类的支持:
ExternalResource(例如TemporaryFolder)Verifier(例如ErrorCollector)ExpectedException
通过使用org.junit.jupiter.migrationsupport.rules包中的类级别注释@EnableRuleMigrationSupport,可让使用这些规则的已有代码保持不变。
要在Maven中开启该支持需要添加相关依赖:
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-migrationsupport</artifactId>
<version>5.8.0</version>
</dependency>
</dependencies>
同样的,若使用Gradle需要在build.gradle中添加相关依赖:
dependencies {
testImplementation('org.junit.jupiter:junit-jupiter-migrationsupport:5.8.0')
}
预期异常
在JUnit 4中使用@Test(expected = SomeException.class)注解不允许我们检查异常的详细信息,若需要检查,则要使用ExpectedException规则。
JUnit 5迁移支持允许我们通过在测试中添加@EnableRuleMigrationSupport来仍然使用该规则。
@EnableRuleMigrationSupport
class JUnit5ExpectedExceptionTest {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
void catchThrownExceptionAndMessage() {
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Wrong argument");
throw new IllegalArgumentException("Wrong argument!");
}
}
由于我们将所有内容都集中在一起,结果更具有可读性。
临时目录
在JUnit 4中可通过使用TemporaryFolder规则来创建和清除一个临时目录,同样的,JUnit 5迁移支持允许我们添加@EnableRuleMigrationSupport来继续使用该功能。
@EnableRuleMigrationSupport
class JUnit5TemporaryFolderTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
void shouldCreateNewFile() throws IOException {
File textFile = temporaryFolder.newFile("test.txt");
Assertions.assertNotNull(textFile);
}
}
要完全摆脱JUnit 4中的规则,我们需要将其替换为TempDirectory扩展,可通过给一个Path或File属性添加@TempDir注解来使用此扩展:
class JUnit5TemporaryFolderTest {
@TempDir
Path temporaryDirectory;
@Test
public void shouldCreateNewFile() {
Path textFile = temporaryDirectory.resolve("test.txt");
Assertions.assertNotNull(textFile);
}
}
此扩展与前面的规则类似,一个不同点是我们也可以将其添加到方法参数中:
@Test
public void shouldCreateNewFile(@TempDir Path anotherDirectory) {
Path textFile = anotherDirectory.resolve("test.txt");
Assertions.assertNotNull(textFile);
}
自定义规则
迁移JUnit 4中的规则时需要将其重写为JUnit 5中的扩展。
可通过实现BeforeEachCallback和AfterEachCallback接口来重现引用了@Rule注解的业务逻辑。
例如,我们有一个实现性能日志的JUnit 4规则:
public class JUnit4PerformanceLoggerTest {
@Rule
public PerformanceLoggerRule logger = new PerformanceLoggerRule();
}
public class PerformanceLoggerRule implements TestRule {
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
// Store launch time
base.evaluate();
// Store elapsed time
}
};
}
}
反过来,我们可以编写与JUnit 5扩展相同的规则:
@ExtendWith(PerformanceLoggerExtension.class)
public class JUnit5PerformanceLoggerTest {
}
public class PerformanceLoggerExtension
implements BeforeEachCallback, AfterEachCallback {
@Override
public void beforeEach(ExtensionContext context) throws Exception {
// Store launch time
}
@Override
public void afterEach(ExtensionContext context) throws Exception {
// Store elapsed time
}
}
自定义类规则
类似的,可通过实现BeforeEachCallback和AfterEachCallback接口来重现引用了@ClassRule注解的业务逻辑。
在某些场景中,我们可能在JUnit 4中将类规则编写为匿名内部类,如下述例子,有一个服务器资源我们希望可以很轻松的在不同的测试中使用设置。
public class JUnit4ServerBaseTest {
static Server server = new Server(9000);
@ClassRule
public static ExternalResource resource = new ExternalResource() {
@Override
protected void before() throws Throwable {
server.start();
}
@Override
protected void after() {
server.stop();
}
};
}
public class JUnit4ServerInheritedTest extends JUnit4ServerBaseTest {
@Test
public void serverIsRunning() {
Assert.assertTrue(server.isRunning());
}
}
可将该规则编写为JUnit 5扩展,不幸的是,如果在该扩展中使用@ExtendWith注解,我们无法访问该扩展提供的资源,但可通过使用@RegisterExtension来替换:
public class ServerExtension implements BeforeAllCallback, AfterAllCallback {
private Server server = new Server(9000);
public Server getServer() {
return server;
}
@Override
public void beforeAll(ExtensionContext context) throws Exception {
server.start();
}
@Override
public void afterAll(ExtensionContext context) throws Exception {
server.stop();
}
}
class JUnit5ServerTest {
@RegisterExtension
static ServerExtension extension = new ServerExtension();
@Test
void serverIsRunning() {
Assertions.assertTrue(extension.getServer().isRunning());
}
}
参数化测试
在JUnit 4中编写参数化测试时需要使用Parameterized runner,此外,我们需要通过一个添加了@Parameterized.Parameters注解的方法来传递参数化数据:
@RunWith(Parameterized.class)
public class JUnit4ParameterizedTest {
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {
{ 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 }
});
}
private int input;
private int expected;
public JUnit4ParameterizedTest(int input, int expected) {
this.input = input;
this.expected = expected;
}
@Test
public void fibonacciSequence() {
assertEquals(expected, Fibonacci.compute(input));
}
}
编写JUnit 4参数化测试有很多缺点,并且像JUnitParams这样的社区runner将自己描述为并不糟糕的参数化测试。
不幸的是没有JUnit 4参数化runner的直接替代品,相反的,JUnit 5中提供了一个@ParameterizedTest注解,可以为数据提供各种数据源注解,其中最接近JUnit 4的是@MethodSource注释:
class JUnit5ParameterizedTest {
private static Stream<Arguments> data() {
return Stream.of(
Arguments.of(1, 1),
Arguments.of(2, 1),
Arguments.of(3, 2),
Arguments.of(4, 3),
Arguments.of(5, 5),
Arguments.of(6, 8)
);
}
@ParameterizedTest
@MethodSource("data")
void fibonacciSequence(int input, int expected) {
assertEquals(expected, Fibonacci.compute(input));
}
}
注意
在JUnit 5中与JUnit 4参数化测试最接近的是使用@ParameterizedTest和@MethodSource数据源,但JUnit 5中的参数化测试有多项改进,可在JUnit 5参数化测试中阅读有关改进的更多信息。
总结
从JUnit 4迁移到JUnit 5需要一些工作,具体取决于现有测试的编写方式。
- 我们可以将
JUnit 4测试与JUnit 5测试一起运行,以允许逐步迁移。 - 在很多情况下,我们只需查找并替换包名和类名。
- 我们可能必须将自定义运行程序和规则转换为扩展。
- 要转换参数化测试,我们可能需要做一些返工。
本文的示例代码能在GitHub中找到。