升级到JUNIT5

2,151 阅读15分钟

1 前言

最近工作中负责了一个老项目,发现该项目没有任何单元测试,心中极其忐忑。鉴于该项目在业务中属于横向底层支撑项目,如果没有单元测试,那么将会耗费个人大量的时间去做回归测试;并且,一旦对底层代码进行修改,则会带来很多隐患。因此,想为主要核心功能添加单元测试。 在进行单元测试框架选型中,主要查看了TestNG和Junit。其中发现Junit已经升级到Junit5。Junit5中更新了很多内容。因为以前一直使用Junit框架进行测试,为了减少学习成本,该项目测试框架采用Junit5。

本系列文章翻译自 Baeldung 的《JUnit5 migration guide》电子书。 电子书原件可以从该页面获取 Junit5

2 介绍

《Junit5指南》, 我们知道如何将基于Junit4的测试代码迁移到最新Junit5版本。在本电子书中,我们将开始探讨为什么要迁移到Junit5;探究它的优点,然后我们将了解用于解决兼容性问题的不同能力和并利用JUnit 5中的新特性。

3 Junit5 优点

继Junit4之后,Junit5是基于客服上一版本中的缺点(限制)可开发的:

  • 一个jar包中包含了整个框架功能。即使仅需要其中的一个特性也要将整个包导入。在Junit5,我们更细粒度功能,并且仅需要导入我们需要得功能
  • 在Junit4中,一个测试容器一次仅能执行一个测试用例(例如在Spring Junit4中得ClassRunner 或者Parameterized)。Junit5支持不同得执行执行容器同时执行。
  • Junit4 不在支持java7版本之后得版本,错过了好多Java8得好多特性。Junit5可以很好得利用java8得特性。 让我们开始探究类库是怎么设计得,以及我们应该基于什么目的来导入什么库。

4 管理

为了克服一个包含整个框架得缺点,Junit5由3个主要模块组成:

  • platform平台:作为jvm启动测试框架的基础,并且定义了运行在平台上的开发测试框架的测试引擎接口。
  • Jupiter: 包含了新测试代码编程模型和junit5中用于开发扩展功能的扩充模块。
  • Vintage:提供一个用于兼容Junit4和junit3的测试引擎。 在开始配置Junit5依赖之前,我们需要记住Junit5的运行依赖于Java8。

4.1 配置Junit5

要开始使用Junit5,我们需要在pom.xml文件添加如下依赖:

<dependency>
 	<groupId>org.junit.jupiter</groupId>
 	<artifactId>junit-jupiter-engine</artifactId>
 	<version>5.1.0</version>
 	<scope>test</scope>
</dependency>

或者在build.gradle 文件配置:

testCompile(‘org.junit.jupiter:junit-jupiter-api:5.2.0’)
testRuntime(‘org.junit.jupiter:junit-jupiter-engine:5.2.0’)

4.2 配置老版本Junit

为了减少迁移过程中的疼苦,可以配置Junit vintage,vintage可以支持在Junit5的上下文支持Junit4或者junit3的测试。 我们可以通过以下简单配置来使用vintage:

<dependency>
 	<groupId>org.junit.vintage</groupId>
 	<artifactId>junit-vintage-engine</artifactId>
 	<version>5.2.0</version>
 	<scope>test</scope>
</dependency>

或者build.gradle文件:

testRuntime(‘org.junit.jupiter:junit-vintage-engine:5.2.0’)

5 导入

由于库的不同结构,第一步骤,我们需要全部用新替换所有老版本的导入。
首先,要将

import org.junit.Test;

替换为:

import org.junit.jupiter.api.*;

然后, 还需要替换所有关于断言的导入:

import static org.junit.Assert.*;

替换为:

import static org.junit.jupiter.api.Assertions.*;

由于所有其他的导入被弃用,并且新版本的库发生了一些改变,我们可以安全的移除这些弃用的导入。

6 注解

新库关于注解方面对比Junit4的注解新增了一些改变。我们开始研究基本的注解。

6.1 Before注解

在配置测试时,在升级时,对于基础的注解有一些小改变:

  • @Before注解被@BeforeEach新注解替换。该注解表示了该方法在当前类中被每个被@Test, @RepeatedTest,@ParameterizedTest, @TestFactory注解的方法前执行。
  • @BeforeClass注解被@BeforeAll注解替换,该注解表示该方法了一个需要在被@Before注解的方法前执行。 基于这些改变,如果我们有一些单元测试使用了Junit4的这些注解:
@BeforeClass
static void setup() {
 log.info(“@BeforeClass - executes once before all test methods in this class”);
}
@Before
void init() {
 log.info(“@Before - executes before each test method in this class”);
}

我们需要做一下的改变:

@BeforeAll
static void setup() {
 log.info(“@BeforeAll - executes once before all test methods in this
class”);
}
@BeforeEach
void init() {
 log.info(“@BeforeEach - executes before each test method in this class”);
}

6.2 After注解

同样的,与@Before@BeforeClass注解一样,新版本库中移除了@After@AfterClass注解,并用以下注解进行替换:

  • @AfterEach替换@After注解;注解表示了该方法需要在当前类中被每个被@Test, @RepeatedTest,@ParameterizedTest, @TestFactory注解的方法执行后执行。
  • @AfterClass注解被@AfterAll注解替换,该注解表示该方法了一个需要在被@Before注解的方法执行后执行。 考虑到这些变化,如果我们在一些测试类中有这些Junit4注解:
@After
void tearDown() {
 log.info(“@After - executed after each test method.”);
}

@AfterClass
static void done() {
 log.info(“@AfterClass - executed after all test methods.”);
}

我们需要做以下该改变:

@AfterEach
void tearDown() {
 log.info(“@AfterEach - executed after each test method.”);
}

@AfterAll
static void done() {
 log.info(“@AfterAll - executed after all test methods.”);
}

6.3 Test异常

考虑到Junit5伴随注解中导入的改变,其中最重要的时我们不在使用@Test注解来定义异常。 比如在Junit4中:

@Test(expected = Exception.class)
public void shouldRaiseAnException() throws Exception {
 // ...
}

现在,我们使用新的断言方法来断言异常:

public void shouldRaiseAnException() throws Exception {
 Assertions.assertThrows(Exception.class, () -> {
 //...
 });
}

Junit4的超时属性:

@Test(timeout = 1)
public void shouldFailBecauseTimeout() throws InterruptedException {
 Thread.sleep(10);
}

在Junit5中使用新的断言方式来断言超时:

@Test
public void shouldFailBecauseTimeout() throws InterruptedException {
 Assertions.assertTimeout(Duration.ofMillis(1), () -> Thread.sleep(10));
}

6.4 关闭测试

Junit5不在支持Junit4中用于跳过指定测试或者套件测试的@Ignore注解。 在原来的位置,在关闭测试或者套件测试时,我们需要使用@Disabled注解:

@Test
@Disabled
void disabledTest() {
 assertTrue(false);
}

我们可以使用这个注解来替换测试方法或者测试类中的@Ignore注解。

7 断言

在新版本的库中,断言包从org.junit.Assert迁移到org.junit.jupiter.api.Assertions中。 然而, Junit5以依旧保留Junit4中的许多断言方法,同时添加了一些支持Java8新特性的新方法。 与之前的版本一样,不同的断言可以被所有的基础数据,类型和数组使用(包括基础类型和对象)。

7.1 断言信息

断言的一个重大变化是参数顺序的变化,需要将输出信息移动到最后一个参数位置上。 由于这个原因,如果我们定义了为一个断言定义了一个错误信息:

@Test
public void whenAssertingArraysEquality_thenEqual() {
 char[] expected = { ‘J’, ‘u’, ‘n’, ‘i’, ‘t’ };
 char[] actual = “JUnit”.toCharArray();

 assertArrayEquals(“Arrays should be equal”, expected, actual);
}

我们需要做以下移动:

@Test
public void whenAssertingArraysEquality_thenEqual() {
 char[] expected = { ‘J’, ‘u’, ‘n’, ‘i’, ‘t’ };
 char[] actual = “JUnit”.toCharArray();
 assertArrayEquals(expected, actual, “Arrays should be equal”);
}

7.2 消息发布

由于支持Java8,输出信息可以作为发布者,允许对他进行延迟计算。 在该案例中,我们需要在升级过程中增强我们的测试方法,我们可以使用流式消息:

@Test
public void whenAssertingArraysEquality_thenEqual() {
 char[] expected = { ‘J’, ‘u’, ‘n’, ‘i’, ‘t’ };
 char[] actual = “JUnit”.toCharArray();

 assertArrayEquals(expected, actual, () -> “Arrays should be equal”);
}

7.3 组断言

Junit4其中的一个问题是所有的断言都是有序执行的,并且如果其中一个断言失败,后续的所有断言将不会执行。

@Test
public void givenMultipleAssertion_whenAssertingAll_thenOK() {
 assertEquals(“4 is 2 times 2”, 4, 2 * 2);
 assertEquals(“java”, “JAVA”.toLowerCase());
 assertEquals(“null is equal to null”, null, null);
}

Junit引入了新的断言assertAll解决了这个问题。该断言可以创建组断言,该组断言都会被执行,并且将汇总所有断言失败的结果。具体来说,该断言可以设置一个包含了负责失败错误的字符串信息和一个执行流。下边代码展示了我们如何定义一个组断言。

@Test
public void givenMultipleAssertion_whenAssertingAll_thenOK() {
 assertAll(
 “heading”,
 () -> assertEquals(4, 2 * 2, “4 is 2 times 2”),
 () -> assertEquals(“java”, “JAVA”.toLowerCase()),
 () -> assertEquals(null, null, “null is equal to null”)
 );
}

当某一个断言执行过程中发生严重异常(比如内存溢出错误),该组断言会被中止。

7.4 过时断言

在Junit5中移除了部分在可Junit4版本中的断言, 比如:

assertEquals(String message, double expected, double actual)
assertEquals(String message, Object[] expecteds, Object[] actuals)

但是,我们可以使用Junit4中被标记为过时的断言,因为Junit5支持这个两个断言,并且不在被标记为过时断言。

assertEquals(double expected, double actual)
assertEquals(Object[] expecteds, Object[] actuals)

7.5 升级AsserThat

值得注意的是,Junit5中不在支持使用AssertThat。所以如果我们在测试方法中使用了这个断言,那么编译将失败。

@Test
public void testAssertThatHasItems() {
 assertThat(
 Arrays.asList(“Java”, “Kotlin”, “Scala”),
 hasItems(“Java”, “Kotlin”));
}

然而,我们不需要重写这个这个测试方法。因为可以通过Hamcrest测试库中提供的AssertThat来替换。 因此,我们仅需要做一个简单的替换,将下边的导入:

import static org.junit.Assert.assertThat;

改为:

import static org.hamcrest.MatcherAssert.assertThat;

关于断言AssertThat使用对象匹配的其他使用方法, 可以在该篇文章获取《使用HamCrest测试》

8 假定

当我们只想执行满足指定条件的单元测试时,我们可以使用假定方法。这个通常用于正常运行测试的外部条件与测试内容不直接相关。 新类Assumptions定义在 org.junit.jupiter.api.Assumptions,而不是org.junit.Assume中了。 Assumptions支持仅执行Junit4中少数假定:assertTrue, assertFalse。其他假定(assumeNoException, assumeNotNull, assertThat)将不会在Junit5中保留,也不能在新的版本库使用,而采用新的引入assumingThat替换。 以下案例中,我们使用了assumeTrue,assumeFalse。这并不需要对代码做其他的修改,除非我们在断言中定义了一些输出信息。

@Test
public void trueAssumption() {
 assumeTrue(“5 is greater the 1”, 5 > 1);
 assertEquals(5 + 2, 7);
}
@Test
public void falseAssumption() {
 assumeFalse(“5 is less then 1”, 5 < 1);
 assertEquals(5 + 2, 7);
}

9 打标签与过滤

在Junit4中,我们可以通过Category注解进行组测试。在Junit5中,我们通过Tag来替换Category

@Tag(“annotations”)
@Tag(“junit5”)
@RunWith(JUnitPlatform.class)
public class AnnotationTestExampleUnitTest {
 /*...*/
}

我们可以需要使用maven-surefire-plugin来引入/排除这些标签。

<build>
 <plugins>
 <plugin>
 <artifactId>maven-surefire-plugin</artifactId>
 <configuration>
 <properties>
 <includeTags>junit5</includeTags>
 </properties>
 </configuration>
 </plugin>
 </plugins>
</build>

10 显示的名称

由于在Junit5新引入新的注解DisplayName,我们为一个测试类和测试方法重写定义一个名称,这个名称可以在运行时和测试报告中展示。

@DisplayName(“Test case for assertions”)
public class AssertionUnitTest {
 @Test
 @DisplayName(“Arrays should be equals”)
 public void whenAssertingArraysEquality_thenEqual() {
 char[] expected = {‘J’, ‘u’, ‘p’, ‘i’, ‘t’, ‘e’, ‘r’};
 char[] actual = “Jupiter”.toCharArray();
 assertArrayEquals(expected, actual, “Arrays should be equal”);
 }
 @Test
 @DisplayName(“The area of two polygons should be equal”)
 public void whenAssertingEquality_thenEqual() {
 float square = 2 * 2;
 float rectangle = 2 * 2;
 assertEquals(square, rectangle);
 }
}

这个单独注解允许我们改进测试报告内容,编写更详细和更易度的文档。

11 嵌套测试

通过引入嵌套测试,我们可以表达不同测试组之间的复杂关系。其语法非常简单,所有我们需要做的事情,仅仅在内部类中使用Nested注解进行标识。
通过这个新注解,来看看如何创建执行一个具有层次的测试。

public class NestedUnitTest {
 Stack<Object> stack;
 @Test
 @DisplayName(“is instantiated with new Stack()”)
 void isInstantiatedWithNew() {
 new Stack<>();
 }
 @Nested
 @DisplayName(“when new”)
 class WhenNew {
 @BeforeEach
 void init() {
 stack = new Stack<>();
 }
 @Test
 @DisplayName(“is empty”)
 void isEmpty() {
 Assertions.assertTrue(stack.isEmpty());
 }
 @Test
 @DisplayName(“throws EmptyStackException when popped”)
 void throwsExceptionWhenPopped() {
 assertThrows(EmptyStackException.class, () -> stack.pop());
 }
 @Test
 @DisplayName(“throws EmptyStackException when peeked”)
 void throwsExceptionWhenPeeked() {
 assertThrows(EmptyStackException.class, () -> stack.peek());
 }
 @Nested
 @DisplayName(“after pushing an element”)
 class AfterPushing {
 String anElement = “an element”;
 @BeforeEach
 void init() {
 stack.push(anElement);
 }
 @Test
 @DisplayName(“it is no longer empty”)
 void isEmpty() {
 Assertions.assertFalse(stack.isEmpty());
 }
 @Test
 @DisplayName(“returns the element when popped and is empty”)
 void returnElementWhenPopped() {
 Assertions.assertEquals(anElement, stack.pop());
 Assertions.assertTrue(stack.isEmpty());
 }
 @Test
 @DisplayName(“returns the element when peeked but remains not empty”)
 void returnElementWhenPeeked() {
 Assertions.assertEquals(anElement, stack.peek());
 Assertions.assertFalse(stack.isEmpty());
 }
 }
 }
}

使用这种类型的结构,输出内容也将是分层的,这也表明测试体也是层次类型的。

12 条件测试执行

在Junit5中引入了执行条件的概念。条件执行为程序化条件测试定义了扩展接口。基于确定的程序条件,让是否开启测试类或者测试方法的执行成为可能。
在下边案例中,我们定义了为一个测试类和测试方法定义了多个条件执行的扩展方法,一旦条件方法返回禁用结果,我们就关闭了指定的测试方法。

12.1 操作系统条件

我们通过@EnableOnOs@DisableOnOs两个注解来决定是否开启一个基于指定的操作系统的测试类或者测试方法。 以下代码表示怎么运用这些注解。

@Test
@EnabledOnOs({ OS.MAC })
void whenOperatingSystemIsMac_thenTestIsEnabled() {
 assertEquals(5 + 2, 7);
}
@Test
@DisabledOnOs({ OS.WINDOWS })
void whenOperatingSystemIsWindows_thenTestIsDisabled() {
 assertEquals(5 + 2, 7);
}

12.2 java运行环境条件

我们可以通过使用@EnableOnJre@DisableOnJre两个注解类打开或关闭基于指定JRE版本的测试类或者测试方法。
以下代码为示例(一个Java8 一个Java9):

@Test
@EnabledOnJre({ JRE.JAVA_8 })
void whenRunningTestsOnJRE8_thenTestIsEnabled() {
 assertTrue(5 > 4, “5 is greater the 4”);
 assertTrue(null == null, “null is equal to null”);
}
@Test
@DisabledOnJre({ JRE.JAVA_10})
void whenRunningTestsOnJRE10_thenTestIsDisabled() {
 assertTrue(5 > 4, “5 is greater the 4”);
 assertTrue(null == null, “null is equal to null”);
}

12.3 系统参数条件

在本例子中,我们可以通过@EnableIfSystemProperty@DisableIfSystemProperty的注解来开启或关闭基于JVM系统参数的测试类或者测试方法。

@Test
@EnabledIfSystemProperty(named = “os.arch”, matches = “.*64.*”)
public void whenRunningTestsOn64BitArchitectures_thenTestIsDisabled() {
 Integer value = 5; // result of an algorithm
 assertNotEquals(0, value, “The result cannot be 0”);
}
@Test
@DisabledIfSystemProperty(named = “ci-server”, matches = “true”)
public void whenRunningTestsOnCIServer_thenTestIsDisabled() {
 Integer value = 5; // result of an algorithm
 assertNotEquals(0, value, “The result cannot be 0”);
}

该注解通过matches属性,将属性值解释为正则表达式。

12.4 环境变量条件

在新增了@EnableIfEnviroment@DisableIfEnviroment两个注解后,我们可以开启和关闭基于底层系统设定值的环境变量的测试类或者测试方法:

@Test
@EnabledIfEnvironmentVariable(named = “ENV”, matches = “staging-server”)
public void whenRunningTestsStagingServer_thenTestIsEnabled() {
 char[] expected = {‘J’, ‘u’, ‘p’, ‘i’, ‘t’, ‘e’, ‘r’};
 char[] actual = “Jupiter”.toCharArray();
 assertArrayEquals(expected, actual, “Arrays should be equal”);
}
@Test
@DisabledIfEnvironmentVariable(named = “ENV”, matches = “.*development.*”)
public void whenRunningTestsDevelopmentEnvironment_thenTestIsDisabled() {
 char[] expected = {‘J’, ‘u’, ‘p’, ‘i’, ‘t’, ‘e’, ‘r’};
 char[] actual = “Jupiter”.toCharArray();
 assertArrayEquals(expected, actual, “Arrays should be equal”);
}

12.5 休眠条件

如果我们想在不开启任何特定条件下执行一个运行套件,那么我们可以简单的使用junit.jupiter.conditions.deactivate配置一个简单的模板,用于指定我们想禁用的条件。
举个例子,我们可以在启动jvm时添加特定参数,便可以运行所有的单元测试,即使有一些使用@Disable进行注解。

-Djunit.jupiter.conditions.deactivate=org.junit.*DisabledCondition

13 扩展

与Junit4相比,使用了Junit Jupiter扩展模型, 运行器,@Runner@ClassRule扩展点由一些单一的,一致的概念组成,被标明为所有扩展的标记接口。

13.1 扩展模型

Junit5的扩展与测试中的指定执行事件相关,这个执行时间称为扩展点。当测试执行到生命周期的指定确切点时,Junit执行引擎将调用执行注册的扩展点。 我们可以使用5个主要扩展点类型:

  • 测试运行后的处理
  • 条件测试执行
  • 生命周期中的回调
  • 参数方案解决
  • 异常处理 有一点需要注意,即这只是与Jupiter引擎相关,与Junit5引擎不与Jpiter引擎共享同一的扩展模型。
    下表将说明如何注册一个扩展点。

13.2 注册扩展

在junit4中,我们使用@RunWith将测试的上下文与其他款框架集成起来,或者改变集成测试中的执行流。
在Junit5中,我们将使用@ExtendWith注解来提供类似的功能。 在Junit4中使用Spring框架特征:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
 {“/app-config.xml”, “/test-data-access-config.xml”})
public class SpringExtensionTest {
 /*...*/
}

现在,在JUnit5中将以下使用:

ExtendWith(SpringExtension.class)
@ContextConfiguration(
  { “/app-config.xml”, “/test-data-access-config.xml” })
public class SpringExtensionTest {
 /*...*/
}

注解@ExtendWith注解允许任何实现了Extension接口的类。
扩展模型的其他内容,扩展的创建以及可用的扩展在Junit5扩展入门

14 规则

在Junit4中,通过使用@Rule@ClassRule注解来为测试新增自定义功能。
在新版本(Junit5)库中不再支持规则相关的定义。但是,为了实现渐进式迁移,Junit团队决定支持部分Junit4规则。
让我们探究那些支持的规则。

14.1 支持的规则

Junit5通过适配器支持少数的规则,并且只考虑那些在语义上与Junit Jupiter扩展模型相匹配的规则。
因此,支持仅包括那些不完全改变测试整体流程的规则。 首先, 我们在pom.xml文件中新增依赖。

<dependency>
 <groupId>org.junit.jupiter</groupId>
 <artifactId>junit-jupiter-migrationsupport</artifactId>
 <version>${junit.vintage.version}</version>
 <scope>test</scope>
</dependency>

在我们配置了依赖后,我们便支持三种类型的规则,包括他们的子类。

  • org.junit.rules.ExternalResource (包括 org.junit.rules.TemporaryFolder)
  • org.junit.rules.Verifier (包括org.junit.rules.ErrorCollector)
  • org.junit.rules.ExpectedException 我们可以通过开启类级别注解org.junit.jupiter.migrationsupport.rulesEnableRuleMigrationSupport来使用这种受限的规则支持。
@EnableRuleMigrationSupport
public class RuleMigrationSupportUnitTest {
 @Rule
 public ExpectedException exceptionRule = ExpectedException.none();
 @Test
    public void whenExceptionThrown_thenExpectationSatisfied() {
 exceptionRule.expect(NullPointerException.class);
 String test = null;
 test.length();
 }
 @Test
 public void whenExceptionThrown_thenRuleIsApplied() {
 exceptionRule.expect(NumberFormatException.class);
 exceptionRule.expectMessage(“For input string”);
 Integer.parseInt(“1a”);
 }
}

由于Junit Jupiter支持Junit4规则是一个实验型特性,如果我们为Junit5开发新的扩展,我们应该使用Junit Jupiter的新模型而不是使用Junit4的基于规则模型。

14.2 升级规则到扩展

Junit5中,我们可以使用@ExtendWith注解类重写同样的逻辑。
比如:我们在Junit4中自定义了一个规则用于追踪测试执行前后的日志链路。

public class TraceUnitTestRule implements TestRule {
 
@Override
 public Statement apply(Statement base, Description description) {
 return new Statement() {
 @Override
 public void evaluate() throws Throwable {
 // Before and after an evaluation tracing here 
 ...
 }
 };
 }
}

然后,我们在测试套件中实现该接口:

@Rule
public TraceUnitTestRule traceRuleTests = new TraceUnitTestRule();

在Junit5中,我们可以用更加直观的形式来实现相同的逻辑。

public class TraceUnitExtension implements AfterEachCallback, 
BeforeEachCallback {
 
 @Override
 public void beforeEach(TestExtensionContext context) throws Exception {
 // ...
 }
 
 @Override
 public void afterEach(TestExtensionContext context) throws Exception {
 // ...
 }
}

使用Junit5中org.junit.jupiter.api.extension包中可用的AfterEachCallbackBeforeEachCallback接口,我们容易的在测试套件中实现这个规则。

@RunWith(JUnitPlatform.class)
@ExtendWith(TraceUnitExtension.class)
public class RuleExampleTest {
 
 @Test
 public void whenTracingTests() {
 /*...*/
 }
}

15 总结

在本电子书中,我们简述了从Junit4迁移到Junit5中的所有步骤,和如果利用Junit5中不同的增强功能来提升我们的测试。

很多测试专有的词汇不知道怎么翻译,麻烦各位轻喷,请大家多多提建议。