Migrating from JUnit 4 to JUnit 5: Important Differences and Benefits
JUnit5简述
适应软件工程的发展,JUnit5在先前版本上演化出来,功能改进和提供新特性,为了更好的服务于测试。一般使用而言:只需更新依赖项即可。
JUnit5提供了Vintage
库运行JUnit4的测试而无需对测试用例做任何修改。
junit-vintage-engine 是 JUnit 4 中使用的测试引擎
junit-jupiter-engine 是 JUnit 5 中使用的测试引擎
看完以下四点介绍,再决定是否使用JUnit5也不迟:
- 使用了java语言高版本的特性,如lambda函数。使得测试更强大也更易于维护;
- 为描述、组织、执行测试添加了一些非常有用的新特性,例如,测试可以获得更好的显示名称,并且可以分层组织;
- 按照特性分成独立的依赖包,使用中只需导入需要的特性依赖即可,如使用Maven和Gradle构建的工程,导入依赖是很方便的;
- 可以同时使用多个扩展,而JUnit4一次只能使用一个runner,这意味着可以很容易的将Spring扩展和其他扩展结合起来;
从JUnit4切换到JUnit5不是很麻烦,对不使用新特性而言,一般无需对旧的测试用例做多少更改,切换步骤如下:
- 更新依赖项(maven或gradle的配置文件),注意要保留
junit-vintage-engine
依赖以运行JUnit4的测试用例; - 新的测试用例可以使用JUnit5构建编写了;
主要区别
两者使用起来差别不大,一些需要额外留意的如下:
路径
JUnit 5 使用新的包结构org.junit.jupiter
.如org.junit.Test
换成org.junit.jupiter.api.Test
注解
注解@Test
不再有参数,参数移到方法体中。
如JUnit4中测试预期抛出异常样例:
@Test(expected = Exception.class)
public void testThrowsException() throws Exception {
// ...
}
在JUnit5中,使用如下:
@Test
void testThrowsException() throws Exception {
Assertions.assertThrows(Exception.class, () -> {
//...
});
}
同样的,常用参数timeout
在JUnit4中如下:
@Test(timeout = 10)
public void testFailWithTimeout() throws InterruptedException {
Thread.sleep(100);
}
在JUnit5中,使用如下:
@Test
void testFailWithTimeout() throws InterruptedException {
Assertions.assertTimeout(Duration.ofMillis(10), () -> Thread.sleep(100));
}
常用注解变动如下:
JUnit4 | JUnit5 |
---|---|
@Before | @BeforeEach |
@After | @AfterEach |
@BeforeClass | @BeforeAll |
@AfterClass | @AfterAll |
@Ignore | @Disabled |
@Category | @Tag |
@Rule
和 @ClassRule
使用 @ExtendWith
和 @RegisterExtension
替代。
断言
JUnit5中断言路径是org.junit.jupiter.api.Assertions
。多数常见断言,如assertEquals()
,assertNotNull()
,虽然看起来没变,但是存在一些变动:
- 参数中错误信息移到最后,如
assertEquals("error msg", 1, 2)
变成assertEquals(1, 2, "error msg")
- 多数断言支持lambda形式的错误信息,在断言失败的时候调用;
assertTimeout()
和assertTimeoutPreemptively()
取代JUnit4中的@Timeout
注解(JUnit5中的@Timeout
注解功能作用和JUnit4中的@Timeout
是不一样的).
假设
假设的包路径是org.junit.jupiter.api.Assumptions
。支持先前的假设及使用,同时提供了支持BooleanSupplier
及Hamcrest matchers匹配条件,支持条件匹配上执行lambda表达式。如JUnit4中的一个例子:
@Test
public void testNothingInParticular() throws Exception {
Assume.assumeThat("foo", is("bar"));
assertEquals(...);
}
在JUnit5中可以写成如下:
@Test
void testNothingInParticular() throws Exception {
Assumptions.assumingThat("foo".equals(" bar"), () -> {
assertEquals(...);
});
}
JUnit的扩展
在JUnit4中,一般使用注解@RunWIth
指定运行环境,是JUnit提供给其他框架测试环境接口扩展。使用多个运行器是有问题的,通常需要chaining
或者注解@Rule
。在JUnit5中对扩展进行了简化和改进。
在JUnit4中使用spring框架构建测试如下:
@RunWith(SpringJUnit4ClassRunner.class)
public class MyControllerTest {
// ...
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Inherited
public @interface RunWith {
Class<? extends Runner> value();
}
在JUnit5中使用Spring extension
替代:
@ExtendWith(SpringExtension.class)
class MyControllerTest {
// ...
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Repeatable(Extensions.class)
@API(
status = Status.STABLE,
since = "5.0"
)
public @interface ExtendWith {
Class<? extends Extension>[] value();
}
注解@ExtendWith
是可重复的, 意味着多个扩展可以方便的组合在一起。
如何自定义扩展呢?
通过实现包org.junit.jupiter.api.extension
里的一个或多个接口,然后使用注解@ExtendWith
添加到测试样例中。
换成JUnit5
将JUnit4测试用例转换到JUnit5,对多数用例而言一般步骤如下:
- 修改包名路径,移除JUnit4,换成JUnit5。如注解
@Test
的路径,Asserts
改成Assertions
等; - 全局替换注解和类名,如
@Before
换成@BeforeEach
,就使用IDE工具而言,更换了依赖后,旧的注解或类名会有错误提示的; - 注意
assertions
(消息参数移到最后),timeout
,expected exceptions
,具体见上文说明; - 更新
assumptions
; - 用注解
@ExtendWith
替换@RunWith
、@Rule
或@ClassRule
,以及解决替换后的问题;
Note that migrating parameterized tests will require a little more refactoring, especially if you have been using JUnit 4
Parameterized
(the format of JUnit 5 parameterized tests is much closer toJUnitParams
).
新特性
上文说明的是已有的特性功能及变动,JUnit5还提供了不少新的特性,让测试用例的更具有描述性和维护性。
定义描述信息
可以在类或方法中使用注解@DisplayName
定义描述信息,使得描述测试的目的和追踪失败更容易,如:
@DisplayName("Test MyClass")
class MyClassTest {
@Test
@DisplayName("Verify MyClass.myMethod returns true")
void testMyMethod() throws Exception {
// ...
}
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(
status = Status.STABLE,
since = "5.0"
)
public @interface DisplayName {
String value();
}
更多信息:See the JUnit document for specifics and examples.
断言
JUnit5提供了新的断言,如下:
- assertIterableEquals() 对
Iterable<?>
遍历equals(),如果元素是Iterable
递归继续; - assertLinesMatch() 对字符列表(list,stream)匹配;
- assertAll() 所有断言一起执行,失败的不影响其他执行;
- assertThrows() 和assertDoesNotThrow() 替代注解
@Test
里的expected property
@Nested
允许你有一个内部类,它本质上是一个测试类,允许你在下面组合几个测试类相同的父级(具有相同的初始化,如:
@DisplayName("Verify MyClass")
class MyClassTest {
MyClass underTest;
@Test
@DisplayName("can be instantiated")
public void testConstructor() throws Exception {
new MyClass();
}
@Nested
@DisplayName("with initialization")
class WithInitialization {
@BeforeEach
void setup() {
underTest = new MyClass();
underTest.init("foo");
}
@Test
@DisplayName("myMethod returns true")
void testMyMethod() {
assertTrue(underTest.myMethod());
}
}
}
使用displayname
在测试报告中描述测试的目的以及结构关系
@ParameterizedTest
JUnit4中已经存在,内置的如JUnit4Parameterized
或者第三方的JUnitParams
。而在JUnit5中,借鉴两者好的特性以此完全内置了。如下:
@ParameterizedTest
@ValueSource(strings = {"foo", "bar"})
@NullAndEmptySource
void myParameterizedTest(String arg) {
underTest.performAction(arg);
}
形式看起来类似JUnitParams
,参数直接传递给测试方法。需要注意测试的值可以来自多个不同的源。例子中只用了一个参数,所以@ValueSource
很方便。还有@EmptySource
空字符,@NullSource
空值,其他的还有如@EnumSource
、@ArgumentsSource
,多参数的如@MethodSource
、@CsvSource
另一个添加的测试类型是@RepeatedTest
,一个测试重复执行指定次数。
条件执行
JUnit5提供了ExecutionCondition
扩展api来有条件的启用或停用一个测试或类。如同在测试上使用注解@Disabled
一样,这里提供了判断条件。内置的条件如下:
- @EnabledOnOs 和@DisabledOnOs: 指定的操作系统
- @EnabledOnJre and @DisabledOnJre: 指定的jre版本
- @EnabledIfSystemProperty: 如果满足JVM系统属性值启用
- @EnabledIf: 如果If条件满足启用
测试模板
测试模板不是常规测试;它们定义了一组要执行的步骤,然后可以在其他地方使用特定的调用上下文执行。 For details and examples, see the documentation.
动态测试
动态测试就像测试模板,要运行的测试是在运行时生成的。然而,测试模板是用一组特定的步骤来定义并运行多次,而动态测试使用相同的调用上下文,但可以执行不同的逻辑。动态测试的一个用途是将一个抽象对象的列表流化,并根据它们的具体类型为每个对象执行一组单独的断言。There are good examples in the documentation.