renaissance of junit - introduction to junit 5
TRANSCRIPT
Agenda• Why JUnit 5• JUnit Lambda• JUnit 5 architecture• JUnit the tool• JUnit the platform• Demo• Summary
Why Rewrite JUnit• JUnit 4.0 was released a decade ago• At the time of Java 5 and now java 9 is almost out
• Modularity - big ball of mud• Test discovery and execution - tightly coupled• Extensibility• Runner API
• Entire testing lifecycle – heavyweight• Only one runner allowed
• Rule API• limited to executing some code before, during, and after a test
was run
JUnit 4 modularity• Single junit.jar rules them all
JUnit Lambda• Crowdfunding
campaign• The code name of
JUnit 5• Main contributors:• Johannes Link • Marc Philipp• Matthias Merdes• Stefan Bechtold• Sam Brannen
JUnit 5 Overview• JUnit Platform + JUnit Jupiter + JUnit Vintage• JUnit Platform: TestEngine and Launcher APIs• JUnit Juptier: JUnit 5 TestEngine, new programming and
extension model• JUnit Vintage: TestEngine for running JUnit 3 & 4
• Modular• Extensible• Modern• Backward compatible
Architecture• JUnit the tool• APIs for writing tests
• JUnit the platform• TestEngines for running
tests• Launchers for
discovering and executing test via test engines
JUnit the ToolImproved Testing APIs
Annotations• @Test• @TestFactory• @DisplayName• @BeforeEach• @AfterEach• @BeforeAll• @AfterAll• @Nested
• @Tag• @Disabled• @ExtendWith
Meta-Annotations@Target({ ElementType.TYPE, ElementType.METHOD })@Retention(RetentionPolicy.RUNTIME)@Test@Tag("fast")@interface Fast {}
@Fast void fastTest() { // test body}
Assertions• assertEquals(), assertNotNull(), etc.• assertThrows() and expectThrows• assertAll()• Message is now the last parameter• Lambda syntax
Assertions@Testvoid groupedAssertions() { assertAll("address", () -> assertEquals("John", address.getFirstName()), () -> assertEquals("User", address.getLastName()), () -> assertThrows(RuntimeException.class, () -> address.getZipcode()) );}
Assumptions• Abort test if assumptions fail.@Testvoid testOnlyOnCiServer() { assumeTrue("CI".equals(System.getenv("ENV"))); // remainder of test}
@Testvoid testOnlyOnDeveloperWorkstation() { assumeTrue("DEV".equals(System.getenv("ENV")), () -> "Aborting test: not on developer workstation"); // remainder of test}
Tagging and Filtering• Be used to filter test discovery and execution@Tag("fast")@Tag("model")class TaggingDemo {
@Test @Tag("taxes") void testingTaxCalculation() { }
}
Nested Tests• Express logical test group• No @BeforeAll and @AfterAll in nested testsclass OuterTest {
@Nested class InnerTest {
@BeforeEach void pushAnElement() { // test body } }}
Dependency Injection• No arguments are allowed in previous version of
JUnit in test methods• ParameterResolver• TestInfoParameterResolver• TestReporterParameterResolver
• Applied to test constructor, @Test, @TestFactory, @BeforeEach, @AfterEach, @BeforeAll, @AfterAll methods• Leveraged by extensions
Dependency Injection• TestInfoParameterResolver@BeforeEachvoid init(TestInfo testInfo) { String displayName = testInfo.getDisplayName(); assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()"));}
@Test@DisplayName("TEST 1")@Tag("my tag")void test1(TestInfo testInfo) { assertEquals("TEST 1", testInfo.getDisplayName()); assertTrue(testInfo.getTags().contains("my tag"));}
Dependency Injection• TestReporterParameterResolver@Testvoid reportSingleValue(TestReporter testReporter) { testReporter.publishEntry("a key", "a value");}
@Testvoid reportSeveralValues(TestReporter testReporter) { HashMap<String, String> values = new HashMap<>(); values.put("user name", "dk38"); values.put("award year", "1974");
testReporter.publishEntry(values);}
Dependency Injection• Inject Mockito mocks
@ExtendWith(MockitoExtension.class)class MyMockitoTest {
@BeforeEach void init(@Mock Person person) { when(person.getName()).thenReturn("Dilbert"); }
@Test void simpleTestWithInjectedMock(@Mock Person person) { assertEquals("Dilbert", person.getName()); }}
Interface Default Methods• Enables multiple inheritance in tests• @Test, @TestFactory, @BeforeEach, @AfterEachinterface EqualsContract<T> extends Testable<T> {
T createNotEqualValue();
@Test default void valueEqualsItself() { T value = createValue(); assertEquals(value, value); }}
Dynamic Tests• Test cases are generated at runtime• Via method annotated with @TestFactory
@TestFactoryStream<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)));}
JUnit the PlatformAs the foundation of testing frameworks
Launcher API• Used by IDEs and build tools to launch the
framework• Central API for discovering and executing tests via
one or more engines• LauncherDiscoveryRequest• Slectors and filters• Return a TestPlan
• Feedback provided via the TestExecutionListener API
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() .selectors( selectPackage("com.example.mytests"), selectClass(MyTestClass.class) ) .filters(includeClassNamePattern(".*Test")) .build();
Launcher launcher = LauncherFactory.create();
TestPlan plan = launcher.discover(request);
TestExecutionListener listener = new SummaryGeneratingListener();launcher.registerTestExecutionListeners(listener);
launcher.execute(request);
TestEngine API• Test engine discovers and executes tests• for a particular programming model, E.g. Spock,
ScalaTest• Automatic discovery via Java’s ServiceLoader
mechanism (META-INF/services)• JupiterTestEngine (JUnit 5)• VintageTestEngine (JUnit 3/4)• Your own implementation
Extension Model• @ExtendWith(…)• Extension.class marker interface@ExtendWith({ MockitoExtension.class, FooExtension.class })@ExtendWith(BarExtension.class)class MyTestsV1 { // ...}
Extension Points• ContainerExecutionCondition• TestExecutionCondition• TestInstancePostProcessor• ParameterResolver• TestExecutionExceptionHandler• Before(All|Each|TestExecution)Callback• After(All|Each|TestExecution)Callback
Extension Pointsclass IgnoreIOExceptionExtension implements TestExecutionExceptionHandler {
@Override public void handleTestExecutionException( TestExtensionContext context, Throwable throwable) throws Throwable { if (throwable instanceof IOException) { return; } throw throwable; }}
Spring Extensionprivate DemoBean demoBean;
@Autowiredpublic void setDemoBean(DemoBean demoBean) { this.demoBean = demoBean;}
@Testvoid testBean() { Assertions.assertNotNull(demoBean);}
API Annotations• @API• Internal• Deprecated• Experimental• Maintained• Stable
@API(Experimental)public interface TestExecutionListener { // code body}
DemoAssertions.assertTrue(demo.isSuccessful())
Summary• Java 8 support• Improved testing API• Improved modularity - separate of concern• Easier to maintain and extend• Backward compatible • Aimed to be the foundation of java testing
ecosystem
Reference• JUnit 5 user guide• JUnit 5 - An Early Test Drive – part1, part2• JUnit 5 - from Lambda to Alpha and beyond• JUnit 5 - Shaping the Future of Testing on the JVM