JUnit 5
Overview & new features
by Vlad Gaevsky / EPAM Systems
About Me
- Java developer
- 3 years experience
- Keen on new technologies
Big fan of: Kotlin, Spring Boot, Gradle, Lombok, etc.
@kelstar95
vlad.gaevsky@gmail.com
t.me/kelstar
Current State
Requirements
- Java 8+
Set up
IntelliJ IDEA 2017.1+
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.0.0-M4</version>
</dependency>
Other IDE
+
Plugin
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19</version>
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>
junit-platform-surefire-provider
</artifactId>
<version>1.0.0-M4</version>
</dependency>
</dependencies>
</plugin>
dependencies {
testCompile 'org.junit.jupiter:junit-jupiter-api:5.0.0-M4'
}
What's the same
JUnit 5 | JUnit 4 |
---|---|
@BeforeEach | @Before |
@AfterEach | @After |
@BeforeAll | @BeforeClass |
@AfterAll | @AfterClass |
@Disabled | @Ignore |
What is not the same
@Test
JUnit 5
JUnit 4
org.junit.jupiter.api.Test
org.junit.Test
Same but different
class FirstTests {
@Test
void firstTest() {
fail();
}
}
4.. or 5?
No more public!
@Test (timeout=..., expected=...)
Interface Contract
public interface ComparableContract<T extends Comparable<T>> {
T createValue();
T createSmallerValue();
@Test
default void returnsZeroWhenComparedToItself() {
T value = createValue();
assertEquals(0, value.compareTo(value));
}
@Test
default void returnsPositiveNumberComparedToSmallerValue() {
T value = createValue();
T smallerValue = createSmallerValue();
assertTrue(value.compareTo(smallerValue) > 0);
}
@Test
default void returnsNegativeNumberComparedToSmallerValue() {
T value = createValue();
T smallerValue = createSmallerValue();
assertTrue(smallerValue.compareTo(value) < 0);
}
}
@Tag
@Tag("Test class")
public class TaggedTest {
@Test
@Tag("covfefe")
void testMethod() {
assertEquals(2+2, 4);
}
@Test
@Tag("jenkins")
void jenkinsOnly() {
fail();
}
}
apply plugin: 'org.junit.platform.gradle.plugin'
junitPlatform {
filters {
tags {
include 'jenkins'
}
}
}
@DisplayName
@Test
@DisplayName("Custom test name containing spaces")
void testWithDisplayNameContainingSpaces() {
}
@Test
@DisplayName("╯°□°)╯")
void testWithDisplayNameContainingSpecialCharacters() {
}
@Test
@DisplayName("😱")
void testWithDisplayNameContainingEmoji() {
}
@Nested
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
@BeforeEach
void pushAnElement() {
stack.push("an element");
}
@Test
@DisplayName("it is no longer empty")
void isEmpty() {
assertFalse(stack.isEmpty());
}
}
}
}
Before each in WhenNew
Before each in AfterPushing
@ExtendWith
@Rule
@ClassRule
@RunWith
Rules & Runners
JUnit has two competing extension mechanisms, each with its own limitations.
Rules
- Single rule can't be used for both method and class level callbacks
- No instance processors
Runners
- Very powerful.. even too much
- You cant't combine them
@ExtendWith
@ExtendWith(MockitoExtension.class)
@Test
void mockTest() {
// ...
}
@ExtendWith({ FooExtension.class, BarExtension.class })
class MyTestsV1 {
// ...
}
@ExtendWith(MockitoExtension.class)
class MockTests {
// ...
}
Method level
Class level
Multiple extensions!
Extension Points
- TestInstancePostProcessor
- ParameterResolver
- ContainerExecutionCondition
- TestExecutionCondition
- TestExecutionExceptionHandler
-
BeforeAllCallback
-
BeforeEachCallback
-
BeforeTestExecutionCallback
-
AfterTestExecutionCallback
-
-
AfterEachCallback
-
-
AfterAllCallback
Mockito Extension
public class MockitoExtension implements TestInstancePostProcessor, ParameterResolver {
@Override
public void postProcessTestInstance(Object testInstance, ExtensionContext context) {
MockitoAnnotations.initMocks(testInstance);
}
@Override
public boolean supports(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return parameterContext.getParameter().isAnnotationPresent(Mock.class);
}
@Override
public Object resolve(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return getMock(parameterContext.getParameter(), extensionContext);
}
//...
}
Parameter Resolver
@ExtendWith(MockitoExtension.class)
class MockitoDemoTest {
@BeforeEach
void init(@Mock Person person) {
when(person.getName()).thenReturn("Dilbert");
}
@Test
void simpleTestWithInjectedMock(@Mock Person person) {
assertEquals("Dilbert", person.getName());
}
}
class BuiltInResolversDemo {
@Test
@Tag("my tag")
void test1(TestInfo testInfo) {
assertTrue(testInfo.getTags().contains("my tag"));
}
@Test
void reportSingleValue(TestReporter testReporter) {
testReporter.publishEntry("a key", "a value");
}
}
Conditions
class DisabledCondition implements ContainerExecutionCondition, TestExecutionCondition {
@Override
public ConditionEvaluationResult evaluate(TestExtensionContext context) {
Optional<Disabled> disabled =
findAnnotation(context.getElement(), Disabled.class);
if (disabled.isPresent()) {
return ConditionEvaluationResult.disabled("@Disabled is presented");
}
return ConditionEvaluationResult.enabled("@Disabled is not presented");
}
//The same for ContainerExecutionCondition
}
ContainerExecutionCondition and TestExecutionCondition define the Extension APIs for programmatic, conditional test execution.
Dynamic Tests
class DynamicTestsDemo {
@TestFactory
Collection<DynamicTest> dynamicTestsFromCollection() {
return Arrays.asList(
dynamicTest("1st dynamic test", () -> assertTrue(true)),
dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2))
);
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromIntStream() {
// Generates tests for the first 10 even integers.
return IntStream.iterate(0, n -> n + 2)
.limit(10)
.mapToObj(n ->
dynamicTest("test" + n, () -> assertTrue(n % 2 == 0))
);
}
}
Dynamic Tests
class DynamicTestsDemo {
@TestFactory
Stream<DynamicTest> generateRandomNumberOfTests() {
// Generates random positive integers between 0 and 100 until
// a number evenly divisible by 7 is encountered.
Iterator<Integer> inputGenerator = //implementation
// Returns a stream of dynamic tests.
return DynamicTest.stream(
inputGenerator,
(input) -> "input:" + input, //display name generator
(input) -> assertTrue(input % 7 != 0) //test executor
);
}
}
Repeated Tests
@RepeatedTest(10)
void repeatedTest() {
// ...
}
-
{displayName}
-
{currentRepetition}
-
{totalRepetitions}
@RepeatedTest(value = 3, name = "")
Placeholders
Parametrized Tests
@ParameterizedTest
@ValueSource(strings = { "Hello", "World" })
void testWithStringParameter(String argument) {
assertNotNull(argument);
}
New dependency: junit-jupiter-params
@ValueSource(ints = { 1, 2, 3 })
@ValueSource(doubles = {1.0, 2.0, 3.0})
@ValueSource(longs = {1L, 2L, 3L})
_
@EnumSource
@ParameterizedTest
@EnumSource(TimeUnit.class)
void testWithEnumSource(TimeUnit timeUnit) {
assertNotNull(timeUnit.name());
}
@EnumSource(value = TimeUnit.class, names = "SECONDS")
@MethodSource
@ParameterizedTest
@MethodSource(names = "stringProvider")
void testWithSimpleMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> stringProvider() {
return Stream.of("foo", "bar");
}
@ParameterizedTest
@MethodSource(names = "stringAndIntProvider")
void testWithMultiArgMethodSource(String first, int second) {
assertNotNull(first);
assertNotEquals(0, second);
}
static Stream<Arguments> stringAndIntProvider() {
return Stream.of(
ObjectArrayArguments.create("foo", 1),
ObjectArrayArguments.create("bar", 2)
);
}
@CsvSource
@ParameterizedTest
@CsvSource({ "foo, 1", "bar, 2", "baz, 3" })
void testWithCsvSource(String first, int second) {
assertNotNull(first);
assertNotEquals(0, second);
}
@ParameterizedTest
@CsvFileSource(resources = "/two-column.csv")
void testWithCsvFileSource(String first, int second) {
assertNotNull(first);
assertNotEquals(0, second);
}
foo, 1
bar, 2
baz, 3
two-column.csv
@ArgumentsSource
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
assertNotNull(argument);
}
static class MyArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> arguments(ContainerExtensionContext context) {
return Stream.of("foo", "bar")
.map(ObjectArrayArguments::create);
}
}
Explicit Conversion
@ParameterizedTest
@EnumSource(TimeUnit.class)
void testWithExplicitArgumentConversion(
@ConvertWith(ToStringArgumentConverter.class) String argument) {
assertNotNull(TimeUnit.valueOf(argument));
}
static class ToStringArgumentConverter extends SimpleArgumentConverter {
@Override
protected Object convert(Object source, Class<?> targetType) {
assertEquals(String.class, targetType, "Can only convert to String");
return String.valueOf(source);
}
}
Explicit Conversion
@ParameterizedTest
@ValueSource(strings = { "01.01.2017", "31.12.2017" })
void testWithExplicitJavaTimeConverter(
@JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) {
assertEquals(2017, argument.getYear());
}
@DisplayName("Display name of container")
@ParameterizedTest(name = "{index} ==> first=''{0}'', second={1}")
@CsvSource({ "foo, 1", "bar, 2", "'baz, qux', 3" })
void testWithCustomDisplayNames(String first, int second) {
}
-
{index}
-
{arguments}
-
{0}, {1}
Placeholders
Customizing Display Names
Assumptions
@Test
public void windowsOnly() {
assumeTrue(System.getenv("OS").startsWith("Windows"));
// ...
}
@Test
public void windowsOnlyAction() {
assumingThat(System.getenv("OS").startsWith("Windows"),
() -> {
System.out.println("Wow, it's Windows!'");
});
// ...
}
Assertions
@Test
public void assertions() {
//classic assertEquals
assertEquals("Java", meetup.getLanguage());
//lambda for message
assertTrue(2 == 2, () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily.");
//lambda for condition
assertTrue(() -> "OOP".equals("Java"), "Java should be OOP");
// In a grouped assertion all assertions are executed, and any
// failures will be reported together.
assertAll(
() -> assertEquals("John", address.getFirstName()),
() -> assertEquals("Doe", address.getLastName())
);
}
Timeout
@Test
void timeoutNotExceeded() {
assertTimeout(ofMinutes(2), () -> {
// Perform task that takes less than 2 minutes.
});
}
@Test
void timeoutExceeded() {
// execution exceeded timeout of 10 ms by 91 ms
assertTimeout(ofMillis(10), () -> {
Thread.sleep(100);
});
}
@Test
void timeoutExceededWithPreemptiveTermination() {
// execution timed out after 10 ms
assertTimeoutPreemptively(ofMillis(10), () -> {
Thread.sleep(100);
});
}
Testing Exceptions
@Test
void exceptionTesting() {
Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("a message");
});
assertEquals("a message", exception.getMessage());
}
JUnit 4 -> JUnit 5
The only thing you need:
junit-vintage-engine
Modularity
JUnit 5 = Platform + Jupiter + Vintage
JUnit 5
- Modular
- Extensible
- Modern
- Backward compatible
Links
JUnit 5 Reference
Presentation
Code
What next
- Check manual
- Try it yourself
- Run it on your project
Questions
Junit 5: Overview & new features
By Vlad Gaevsky
Junit 5: Overview & new features
- 4,203