JUnit 5

Overview & new features

by Vlad Gaevsky / EPAM Systems

Current State

Requirements

  • Java 8+

Set up

IntelliJ IDEA 2016.3.1+

<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-api</artifactId>
  <version>5.0.0-M3</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-M3</version>
    </dependency>
  </dependencies>
</plugin>
dependencies {
    testCompile 'org.junit.jupiter:junit-jupiter-api:5.0.0-M3'
}

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("Test method")
    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;

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
    }

    @Nested
    @DisplayName("when new")
    class WhenNew {
        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {
            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @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
  • ContainerExecutionCondition
  • TestExecutionCondition
  • ParameterResolver
  • 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
    @DisplayName("TEST 1")
    @Tag("my tag")
    void test1(TestInfo testInfo) {
        assertEquals("TEST 1", testInfo.getDisplayName());
        assertTrue(testInfo.getTags().contains("my tag"));
    }

    @Test
    void reportSingleValue(TestReporter testReporter) {
        testReporter.publishEntry("a key", "a value");
    }

}

Conditions

class DisabledCondition implements ContainerExecutionCondition, TestExecutionCondition {

	/**
	 * Tests are disabled if {@code @Disabled} is present on the test method.
	 */
	@Override
	public ConditionEvaluationResult evaluate(TestExtensionContext context) {
		Optional<Disabled> disabled = 
                    findAnnotation(context.getElement(), Disabled.class);
		if (disabled.isPresent()) {
			String reason = disabled.map(Disabled::value)
                                          .filter(StringUtils::isNotBlank)
                                          .orElseGet(() -> element.get() + " is @Disabled");
			return ConditionEvaluationResult.disabled(reason);
		}

		return ConditionEvaluationResult.enabled("@Disabled is not present");
	}

        //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 = new Iterator<Integer>() {
            Random random = new Random();
            int current;

            @Override
            public boolean hasNext() {
                current = random.nextInt(100);
                return current % 7 != 0;
            }

            @Override
            public Integer next() {
                return current;
            }
        };

        // Returns a stream of dynamic tests.
        return DynamicTest.stream(
                inputGenerator, 
                (input) -> "input:" + input, //display name generator
                (input) -> assertTrue(input % 7 != 0) //test executor
        );
    }
}

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(() -> "".isEmpty(), "string should be empty");

  // 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

Presentation

JUnit 5 Reference 

Code

Questions

Made with Slides.com