This article shows how get from JUnit 3.x / 4.x to JUnit 5.x as fast as possible.
Just a short clarification of the term “JUnit 5” (from the user guide) before we take off:
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
where
- Platform provides the Maven and Gradle Plugins and is the extension point for IDE integration,
- Vintage contains legacy JUnit 4 API and engine,
- Jupiter contains the new JUnit 5 API and engine.
Step 1 – Run existing tests with JUnit 5 vintage
The first thing we do is to replace the existing junit:junit
depedency with the following
<dependency> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <version>5.1.0</version> </dependency>
For Gradle see this article.
For a real world example see this commit.
Note: After upgrading fromjunit:junit:4.12
to org.junit.vintage:junit-vintage-engine:5.1.0
the execution order of @Rule
seems to have changed: They seem to be now executed sequentially (from top to bottom, as defined in the test class).
Step 2 – Getting started with JUnit Jupiter and the Platform
Now lets go from vintage to the fancy new stuff. Just add the Jupiter dependency and empower surefire to use the JUnit platform:
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.1.0</version> </dependency> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <!-- ... --> <dependencies> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-surefire-provider</artifactId> <version>1.1.0</version> </dependency> </dependencies> </plugin>
Make sure to juse either surefire 2.19.1 or 2.21.0+, as there seem to be a bug in the versions in between.
As above, for Gradle see this article.
For a real world example see this commit.
As of now, we’re ready to write new tests with JUnit Jupiter.
Here’s a pragmatic aproach how to introduce JUnit 5 from here:
- Use the new API and all the new features for new test classes.
- Don’t try to migrate all existing tests. It causes a lot of effort with no direct business value.
- Instead, apply the boyscout rule by gradually migrating existing tests before they need to be changed.
When getting started wiht JUnit Jupiter you will recognize that some familiar features of JUnit now have a new API or can be achieved using different concepts. After that, there are a some new features to explore.
One way to get accustomed to the new API and concrepts is to migrate some (not all) existing tests, preferably the most complex ones. This way, you will find out how to use the new concepts and which limitations there still might be about JUnit Jupiter (e.g JUnit 4 rules that have not been ported to extensions).
Step 3 – Get accustomted to the API changes in JUnit Jupiter
There are some simple API changes but also two major concept changes: Rules and Runners are gone.
Simple API changes
public
modifier can be removed (class and methods)org.junit.Test
➡️org.junit.jupiter.api.Test
org.junit.Assert.assertX
➡️org.junit.jupiter.api.Assertions.assertX
(exceptassertThat
)- Order of parameters changed in
assert
methods. The message parameter is now afterexpected
andactual
parameters! This can be a pitfall when migrating, because themessage
(strings) might silently turn toexpected
, if you just change the import. assertThat
is no longer part of the JUnit API. Instead, just use your favorite assertion library as AssertJ, Google truth or even hamcrest.@Before
➡️@BeforeEach
@After
➡️@AfterEach
@BeforeClass
➡️@BeforeAll
@AfterClass
➡️@AfterAll
@Ignore
➡️@Disabled
@Category
➡️@Tag
For a real world example see this commit.
Make sure to not mix the APIs, because the tests are either run by the Jupiter or the vintage Engine, which will ignore unknown annotations.
Note that IntellI has a quick fix for migrating JUnit 4 to JUnit Jupiter. However, as of version 2018.1 this seems to only affect @Test
, no asserts, exceptions, rules or runners.
Advanced API changes
Basically, Runners and Rules are replaced by Extensions, where one test class can have more than one extension.
However, some Runners have not been ported to Extensions, yet. For those you can try to use@EnableRuleMigrationSupport
(see Temporary Folders). If this does not work, you will have to stick with the JUnit 4 API and vintage Engine for now.
Exceptions & Timeouts
Exceptions no longer need a Rule or the expected
param in @Test
. Instead, the API provides an assert mechanism now.
ExpectedException
and @Test(expected = Exception.class)
➡️ assertThrows(Exception.class,() -> method());
For a real world example see this commit.
The same applies to timeouts:
@Test(timeout = 1)
➡️ assertTimeout(Duration.ofMillis(1), () ->method());
Mockito
Instead of the mockito runner, we use the new extension, which comes in a separate module.
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>${mockito.version}</version> </dependency>
@RunWith(MockitoJUnitRunner.class)
➡️ @ExtendWith(MockitoExtension.class)
For a real world example see this commit.
Temporary Folders
Until there is an Extension, we can use @EnableRuleMigrationSupport
from this module:
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-migrationsupport</artifactId> <version>${junit5.version}</version> </dependency>
With this we can use the new API (org.junit.jupiter.api.Test
). Howerver, rules and classes must stay public. ClassRules seem not to work.
For a real world example see this commit.
Other Rules
Here are some more rules and their equivalent in JUnit Jupiter.
@RunWith(SpringJUnit4ClassRunner.class)
➡️@ExtendWith(SpringExtension.class)
- stefanbirkner/system-rules, such as
ExpectedSystemExit
Work in progress! That is, these tests will have remain on the JUnit 4 APIs for now. TestLoggerFactoryResetRule
from slf4j-test
No progress to be seen.
Could be replaced by logback-spike. For a real world example see this commit.- Of course this list is non-exhaustive, there are a lot more runners I have not stumbled upon, yet.
Step 4 – Make use of new features in JUnit Jupiter
Just using the same features with different API is boring, right?
JUnit Jupiter offers some long-awaited features that we should make use of!
Here are some examples:
- Nested test classes (similar to the JUnit 4
Enclosed
Runner), each with its own@BeforeX
, etc. methods:
@Nested class Inner {
It can now be combined with parameterized and other extensions 🍾
For a real world example see this commit. - Parameterized tests (similar to the JUnit 4
Parameterized
Runner.
@ParameterizedTest
In separate module:<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-params</artifactId> <version>${junit5.version}</version> </dependency>
This can also be combined with nested and other extensions 🍾
- Naming Classes and test methods with
@DisplayName("display name")
- Conditionally test execution, like
@EnabledOnOs(MAC)
,@EnabledOnJre(JAVA_8)
, etc.
Can be extended easily. - Group assertions:
assertAll()
with lambdas. - Dynamic Tests (
@TestFactory
) @RepeatedTest
- Of course, there’s more! See the User Guide.
cool post. A couple of other things maybe worth mentioning:
1) use 2.19.1 version of failsafe + surefire for maven due to bug
2) Don’t mix the imports in a class (jupiter + old junit 4) – it won’t work 😦
3) Intellij has an analyse quick fix that will migrate most tests for you automatically (@RunWith or expected= won’t though)
Lovely, thanks for that. I updated the article.
Hello
is it possible to get more information about the failsafe plugin? I do not get it working.
org.apache.maven.plugins
maven-failsafe-plugin
2.19.1
org.apache.maven.surefire
surefire-junit47
2.19.1
integration-test
verify
This simply throws:
Execution default of goal org.apache.maven.plugins:maven-failsafe-plugin:2.19.1:integration-test failed: There was an error in the forked process
[ERROR] java.lang.NoClassDefFoundError: org/junit/runner/notification/RunListener
Have you had a look at the example?
For me, the surefire-provider also works for failsafe:
https://github.com/schnatterer/colander/blob/7595690205316a4e007d64816b18449769c5fbb0/pom.xml#L231
Should be
@Test(timeout = 1) -> assertTimeoutPreemptively(Duration.ofMillis(1), () ->method());
not
@Test(timeout = 1) ➡️ assertTimeout(Duration.ofMillis(1), () ->method());