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