1 introduction

Recently, I was in charge of an old project and found that there were no unit tests in the project. Considering that the project is a horizontal low-level support project in the business, if there is no unit testing, it will cost a lot of personal time to do regression testing; And once the underlying code is modified, there are many hidden dangers. Therefore, you want to add unit tests for key core functions. In the selection of unit test framework, TestNG and Junit were mainly looked at. Junit has been upgraded to Junit5. There are many updates in Junit5. In order to reduce the learning cost, Junit5 was used as the testing framework of the project because Junit framework was used for testing previously.

This series of articles has been translated from Bae Eldung’s JUnit5 Migration Guide ebook. Ebook originals can be obtained from this page.

2 introduces

In the Junit5 guide, we learned how to migrate junit 4-based test code to the latest Junit5 release. In this ebook, we will begin to explore why we should migrate to Junit5; Explore its benefits, then we’ll look at the different capabilities used to solve compatibility problems and take advantage of the new features in JUnit 5.

3 Junit5 advantages

Following Junit4, Junit5 is developable based on the shortcomings (limitations) in the previous version of customer service:

  • The entire framework functionality is contained in a JAR package. Import the entire package even if only one of the features is needed. In Junit5, we are more fine-grained and import only the functionality we need.
  • In Junit4, a test container can only execute one test case at a time (such as ClassRunner or Parameterized in Spring Junit4). Junit5 supports simultaneous execution of different execution containers.
  • Junit4 is not supported later than java7, missing a lot of Java8 features. Junit5 makes good use of java8 features.

Let’s start exploring how the class libraries are designed and what we should import for what purpose.

4 manage

To overcome the disadvantage of having an entire framework, Junit5 consists of three main modules:

  • Platform: Serves as the basis for the JVM to launch the test framework and defines the test engine interface for developing the test framework that runs on the platform.
  • Jupiter: contains the new test code programming model and the extension module in junit5 for developing extension functionality.
  • Vintage: provides a test engine for compatibility with Junit4 and junit3.

Before we start configuring Junit5 dependencies, we need to remember that Junit5 relies on Java8 to run.

4.1 configuration Junit5

To start using Junit5, we need to add the following dependencies to the pom.xml file:

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

Or configure it in build.gradle:

TestCompile (' org. Junit. Jupiter: junit - Jupiter - API:5.2.0(' ') testRuntime org. Junit. Jupiter: junit - Jupiter - engine:5.2.0')Copy the code

4.2 Configuring Junit of an earlier version

To reduce the pain of migration, Junit Vintage can be configured to support Junit4 or junit3 tests in the context of Junit5. Vintage can be used in the following simple configuration:

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

Or build. Gradle file:

TestRuntime (' org. Junit. Jupiter: junit - vintage - engine: 5.2.0) 'Copy the code

5 the import

Because of the different structure of the library, as a first step, we need to completely replace all the old version imports with new ones. First of all, we have to put

import org.junit.Test;
Copy the code

Replace with:

import org.junit.jupiter.api.*;
Copy the code

Then, we also need to replace all imports on assertions:

import static org.junit.Assert.*;
Copy the code

Replace with:

import static org.junit.jupiter.api.Assertions.*;
Copy the code

Since all the other imports are deprecated, and some changes have been made in the new version of the library, we can safely remove these deprecated imports.

6 annotations

The new library has some new changes in annotations compared to Junit4 annotations. We started to look at basic annotations.

6.1 Before annotations

There are some minor changes to the basic annotations when configuring tests and when upgrading:

  • @BeforeAnnotations are@BeforeEachNew annotation replacement. This annotation indicates that the method is used by each other in the current class@Test.@RepeatedTest.@ParameterizedTest.@TestFactoryAnnotated methods are executed before.
  • @BeforeClassAnnotations are@BeforeAllAnnotation substitution, which indicates that the method has a need in the@BeforeAnnotated methods are executed before.

Based on these changes, if we had some unit tests that used these annotations from Junit4:

@BeforeClass
static void setup(a) {the info ("@BeforeClass - executes once before all test methods in this class").;
}
@Before
void init(a) {the info ("@Before - executes before each test method in this class").;
}
Copy the code

We need to make some changes:

@BeforeAll
static void setup(a) {the info ("@BeforeAll - executes once before all test methods in this
class").;
}
@BeforeEach
void init(a) {the info ("@BeforeEach - executes before each test method in this class").;
}
Copy the code

6.2 After annotation

Also, like the @before and @beforeClass annotations, the @after and @AfterClass annotations are removed from the new library and replaced with the following annotations:

  • @AfterEachreplace@AfterAnnotations; An annotation indicates that the method needs to be used in each of the current classes@Test.@RepeatedTest.@ParameterizedTest.@TestFactoryAnnotated methods are executed after execution.
  • @AfterClassAnnotations are@AfterAllAnnotation substitution, which indicates that the method has a need in the@BeforeAnnotated methods are executed after execution.

Considering these changes, if we had these Junit4 annotations in some of our test classes:

@After
void tearDown(a) {the info ("@After - executed after each test method.”);
}

@AfterClass
static void done(a) {the info ("@AfterClass - executed after all test methods.”);
}
Copy the code

We need to make the following changes:

@AfterEach
void tearDown(a) {the info ("@AfterEach - executed after each test method.”);
}

@AfterAll
static void done(a) {the info ("@AfterAll - executed after all test methods.”);
}
Copy the code

6.3 the Test abnormal

Considering Junit5 comes with changes imported in annotations, one of the most important is that we no longer use the @test annotation to define exceptions. For example, in Junit4:

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

Now, we use a new assertion method to assert exceptions:

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

The timeout attribute of Junit4:

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

Use new assertions in Junit5 to assert timeouts:

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

6.4 Closing a Test

Junit5 does not support the @ignore annotation in Junit4 for skipping specified tests or suite tests. In the old place, we need to use the @disabled annotation when closing tests or suite tests:

@Test
@Disabled
void disabledTest(a) {
 assertTrue(false);
}
Copy the code

We can use this annotation to replace the @ignore annotation in the test method or test class.

7 assertion

In the new version of the repository, Assert that package from org. Junit. Assert migrated to org. Junit. Jupiter. API. Assertions. However, Junit5 retains many of the assertion methods in Junit4, while adding some new methods that support new features in Java8. As in previous versions, different assertions can be used by all underlying data, types, and arrays (both underlying types and objects).

7.1 Assertion Information

A significant change to assertions is a change in the order of arguments that requires moving the output to the last argument position. For this reason, if we define an error message for an assertion:

@Test
public void whenAssertingArraysEquality_thenEqual(a) {
 char[] expected = { ‘J’, ‘u’, ‘n’, ‘i’, ‘t’ };
 char[] actual = "JUnit. ToCharArray (); AssertArrayEquals (" Arrays should be equal ", expected, actual); }Copy the code

We need to make the following moves:

@Test
public void whenAssertingArraysEquality_thenEqual(a) {
 char[] expected = { ‘J’, ‘u’, ‘n’, ‘i’, ‘t’ };
 char[] actual = "JUnit. ToCharArray (); AssertArrayEquals (Expected, actual, "Arrays should be equal"); }Copy the code

7.2 Message Release

Thanks to Java8 support, the output can act as a publisher, allowing it to be evaluated lazily. In this case, we need to enhance our test methods during the upgrade process, we can use streaming messages:

@Test
public void whenAssertingArraysEquality_thenEqual(a) {
 char[] expected = { ‘J’, ‘u’, ‘n’, ‘i’, ‘t’ };
 char[] actual = "JUnit. ToCharArray (); AssertArrayEquals (expected, actual, () -> "Arrays should be equal"); }Copy the code

7.3 set of assertions

One of the problems with Junit4 is that all assertions are executed in order, and if one fails, all subsequent assertions are not executed.

@Test
public void givenMultipleAssertion_whenAssertingAll_thenOK(a) {assertEquals ("4 is 2 times 2",4.2 * 2); AssertEquals (" Java "and" Java ". ToLowerCase ()); AssertEquals ("null is equal to null",null.null);
}
Copy the code

Junit has solved this problem by introducing a new assertion, assertAll. This assertion creates group assertions that are executed and the results of all assertion failures are summarized. Specifically, the assertion can set a string containing information and an execution flow that is responsible for the failure. The following code shows how we define a group assertion.

@Test
public void givenMultipleAssertion_whenAssertingAll_thenOK(a) {
 assertAll(
 “heading”,
 () -> assertEquals(4.2 * 2."4 is 2 times 2"), () - > assertEquals (" Java "and" Java ". ToLowerCase ()), () - > assertEquals (null.null."null is equal to null")); }Copy the code

When a serious exception (such as an out-of-memory error) occurs during the execution of an assertion, the group of assertions is aborted.

7.4 Obsolete Assertions

Remove some of the assertions in Junit4 from Junit5, such as:

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

However, we can use assertions that are marked as obsolete in Junit4, because Junit5 supports both assertions and is no longer marked as obsolete.

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

7.5 upgradeAsserThat

It is worth noting that the use of AssertThat is not supported in Junit5. So if we use this assertion in our test method, the compilation will fail.

@Test
public void testAssertThatHasItems(a) AssertThat (Arrays. AsList (" Java ", "Kotlin", "Scala"), hasItems(" Java ", "Kotlin")); }Copy the code

However, we don’t need to rewrite this test method. Because it can be replaced with AssertThat provided in the Hamcrest test library. So, we just need to do a simple substitution and import the following:

import static org.junit.Assert.assertThat;
Copy the code

To:

import static org.hamcrest.MatcherAssert.assertThat;
Copy the code

For additional uses of AssertThat using object matching, you can get Testing with HamCrest in this article.

Eight assumptions

We can use the hypothetical approach when we only want to execute unit tests that meet specified criteria. This external condition, which is usually used to run tests properly, is not directly related to the test content. New class Assumptions defined in org. Junit. Jupiter. API. Assumptions, rather than org. Junit. Assume. Assumptions in testing support implementation of only a few Assumptions in Junit4: assertTrue, assertFalse. The other assumptions (assumption exception, assumption null, assertThat) will not be retained in Junit5 and will not be used in the new repository and will be replaced with the new introduction of assumption that. In the following case, we use assumeTrue,assumeFalse. This does not require additional changes to the code, unless we define some output in the assertion.

@Test
public void trueAssumption(a) {assumeTrue ("5 is greater the 1",5 > 1);
 assertEquals(5 + 2.7);
}
@Test
public void falseAssumption(a) {assumeFalse ("5 is less then 1",5 < 1);
 assertEquals(5 + 2.7);
}
Copy the code

9 Label and filter

In Junit4, we can do group testing with Category annotations. In Junit5, we replace categories with tags.

@ Tag (" annotations ")
@ Tag (" junit5 ")
@RunWith(JUnitPlatform.class)
public class AnnotationTestExampleUnitTest {
 / *... * /
}
Copy the code

We can use the Maven-Surefire-plugin to introduce/exclude these tags.

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

10 Name displayed

Because of the new annotation DisplayName introduced in Junit5, we define a name for a test class and test method rewrite that can be displayed at run time and in the test report.

@ DisplayName (" Test case for assertions ")
public class AssertionUnitTest {
 @Test
 @ DisplayName (" Arrays should be equals ")
 public void whenAssertingArraysEquality_thenEqual(a) {
 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(a) {
 float square = 2 * 2;
 float rectangle = 2 * 2; assertEquals(square, rectangle); }}Copy the code

This single annotation allows us to improve the content of the test report and write more detailed and easier documentation.

11 Nested tests

By introducing nested tests, we can express complex relationships between different test groups. The syntax is very simple, and all we need to do is identify Nested annotations in the inner class. With this new annotation, see how to create and execute a hierarchical test.

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

With this type of structure, the output content will also be hierarchical, indicating that the test body is also hierarchical.

12 Conditional test execution

The concept of execution conditions was introduced in Junit5. Conditional execution defines an extended interface for programmatic conditional testing. It is possible to enable the execution of test classes or test methods based on defined program conditions. In the following example, we define an extension method that defines multiple conditional executions for a test class and a test method, and when the conditional method returns a disabled result, we close the specified test method.

12.1 Operating System Conditions

We use the @enableonos and @disableonos annotations to decide whether to enable a test class or method based on the specified operating system. The following code shows how to apply these annotations.

@Test
@EnabledOnOs({ OS.MAC })
void whenOperatingSystemIsMac_thenTestIsEnabled(a) {
 assertEquals(5 + 2.7);
}
@Test
@DisabledOnOs({ OS.WINDOWS })
void whenOperatingSystemIsWindows_thenTestIsDisabled(a) {
 assertEquals(5 + 2.7);
}
Copy the code

12.2 Java Runtime Environment Conditions

You can enable or disable the test class or test method based on the specified JRE version by using the @enableonjre and @disableonjre annotation classes. The following code is an example (one Java8 and one Java9) :

@Test
@EnabledOnJre({ JRE.JAVA_8 })
void whenRunningTestsOnJRE8_thenTestIsEnabled(a) {
 assertTrue(5 > 4."5 is greater the 4"); assertTrue(null= =null."null is equal to null"); }@Test
@DisabledOnJre({ JRE.JAVA_10})
void whenRunningTestsOnJRE10_thenTestIsDisabled(a) {
 assertTrue(5 > 4."5 is greater the 4"); assertTrue(null= =null."null is equal to null"); }Copy the code

12.3 System Parameter Conditions

In this example, we can enable or disable test classes or test methods based on JVM system parameters with @enableIfSystemProperty and @DisableIfSystemProperty annotations.

@Test
@enabledifSystemProperty (named = "os.arch", matches = ".*64.* ")
public void whenRunningTestsOn64BitArchitectures_thenTestIsDisabled(a) {
 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(a) {
 Integer value = 5; // result of an algorithm
 assertNotEquals(0, value, “The result cannot be 0"); }Copy the code

This annotation interprets the property value as a regular expression through the matches property.

12.4 Environment Variable Conditions

With @enableifEnviroment and @disableifEnviroment annotations, we can enable and disable test classes or test methods based on environment variables set by the underlying system:

@Test
@ EnabledIfEnvironmentVariable (named = "ENV", matches = "staging - server")
public void whenRunningTestsStagingServer_thenTestIsEnabled(a) {
 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(a) {
 char[] expected = {' J ', 'u', 'p', 'I', 't', 'e', 'r'};char[] actual = "Jupiter". ToCharArray (); AssertArrayEquals (Expected, actual, "Arrays should be equal"); }Copy the code

12.5 Dormancy Conditions

If we want to perform an operation not open any specific conditions suite, then we can use simple junit. Jupiter. The conditions. A simple template, deactivate configuration is used to specify what we want to disable conditions. For example, we can add specific parameters when starting the JVM and run all of our unit tests, even if some are annotated with @disable.

-Djunit.jupiter.conditions.deactivate=org.junit.*DisabledCondition
Copy the code

13 extension

In contrast to Junit4, which uses the Junit Jupiter extension model, the Runner, @Runner, and @classrule extension points consist of a few single, consistent concepts that are identified as the tag interface for all extensions.

13.1 Extending The Model

The extension of Junit5 is related to the specified execution event in the test, which is called the extension point. When the test execution reaches a specified point in the lifecycle, the Junit execution engine invokes the extension point registered for execution. There are five main types of extension points we can use:

  • Processing after the test runs
  • Conditional test execution
  • Callbacks in the lifecycle
  • Parameter Solution
  • Exception handling

It is important to note that this only relates to the Jupiter engine, and the Junit5 engine does not share the same extension model as the Jpiter engine. The following table shows how to register an extension point.

13.2 Registration Extension

In junit4, we used @runwith to integrate the context of our tests with other frameworks, or to change the execution flow in our integration tests. In Junit5, we will use the @extendwith annotation to provide similar functionality. Using Spring framework features in Junit4:

@RunWith(SpringJUnit4ClassRunner.class)
@ ContextConfiguration ({" XML/app - config. ", "/ test - data - access - config. XML"})
public class SpringExtensionTest {
 / *... * /
}
Copy the code

Now, the following will be used in JUnit5:

ExtendWith(SpringExtension.class)
@contextConfiguration ({" /app-config. XML ", "/test-data-access-config.xml"})
public class SpringExtensionTest {
 / *... * /
}
Copy the code

The @extendWith annotation allows any class that implements the Extension interface. The rest of the extension model, extension creation, and available extensions are in Junit5 extension getting started.

14 rules

In Junit4, you add custom functionality to your tests by using the @rule and @classrule annotations. Rules-specific definitions are no longer supported in the new version (Junit5) library. However, in order to achieve progressive migration, the Junit team decided to support some of the Junit4 rules. Let’s explore the supporting rules.

14.1 Supported Rules

Junit5 supports a small number of rules through adapters and considers only those rules that semantically match the Junit Jupiter extension model. Therefore, support includes only rules that do not completely change the overall flow of testing. First, we add a dependency to the POM.xml file.

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

After we configure dependencies, we support three types of rules, including their subclasses.

  • org.junit.rules.ExternalResource(including org.junit.rules.TemporaryFolder)
  • org.junit.rules.Verifier(includingorg.junit.rules.ErrorCollector)
  • org.junit.rules.ExpectedException

We can open the class-level annotations org. Junit. Jupiter. Migrationsupport. RulesEnableRuleMigrationSupport rules to use the limited support.

@EnableRuleMigrationSupport
public class RuleMigrationSupportUnitTest {
 @Rule
 public ExpectedException exceptionRule = ExpectedException.none();
 @Test
    public void whenExceptionThrown_thenExpectationSatisfied(a) {
 exceptionRule.expect(NullPointerException.class);
 String test = null;
 test.length();
 }
 @Test
 public void whenExceptionThrown_thenRuleIsApplied(a) { exceptionRule.expect(NumberFormatException.class); ExceptionRule. ExpectMessage (" For the input string "); Integer. The parseInt (" 1 a "); }}Copy the code

Since Junit Jupiter’s support for Junit4 rules is an experimental feature, if we develop a new extension for Junit5, we should use Junit Jupiter’s new model rather than Junit4’s rules-based model.

14.2 Upgrading Rules to Extension

In Junit5, we can override the same logic using the @extendwith annotation class. For example, in Junit4 we have a custom rule to track log links before and after test execution.

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

We then implement this interface in our test suite:

@Rule
public TraceUnitTestRule traceRuleTests = new TraceUnitTestRule();
Copy the code

In Junit5, we can implement the same logic in a more intuitive form.

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

Using Junit5 org. Junit. Jupiter. API. The extension package available AfterEachCallback and BeforeEachCallback interface, we easy to implement this rule in a test suite.

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

15 summary

In this ebook, we outline all the steps in migrating from Junit4 to Junit5 and how to take advantage of the different enhancements in Junit5 to improve our tests.

Many test proprietary vocabulary do not know how to translate, please gently spray, please give more suggestions.