Slide 1

Slide 1 text

Moving Forward with JUnit 5 Marcel Schnelle @marcelschnelle TenTen Ltd.

Slide 2

Slide 2 text

Everybody knows JUnit.

Slide 3

Slide 3 text

Everybody knows JUnit. class FooTest { @Test fun someTest() { assertEquals(4, 2 + 2) } }

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

dependencies { testImplementation "junit:junit:4.12" } Released in December 2014! (JDK 8: March 2014)

Slide 6

Slide 6 text

July 2015

Slide 7

Slide 7 text

July 2015

Slide 8

Slide 8 text

Rewrite: Why?

Slide 9

Slide 9 text

Two types of limitation inside the current JUnit 4 codebase Use-Site Coupling Ambiguous Evolution

Slide 10

Slide 10 text

Use-Site Coupling Today's tooling depends on JUnit Internals

Slide 11

Slide 11 text

Use-Site Coupling Today's tooling depends on JUnit Internals class FooTest { @Test fun someTest() { assertEquals(5, 2 + 2) } }

Slide 12

Slide 12 text

class FooTest { @Test fun someTest() { assertEquals(5, 2 + 2) } } java.lang.AssertionError: expected:<5> but was:<4> Expected :5 Actual :4 Use-Site Coupling Today's tooling depends on JUnit Internals

Slide 13

Slide 13 text

// junit.framework.ComparisonFailure.java public class ComparisonFailure extends AssertionFailedError { private String fExpected; private String fActual; public ComparisonFailure (String message, String expected, String actual) { super (message); fExpected = expected; fActual = actual; } } Use-Site Coupling Today's tooling depends on JUnit Internals

Slide 14

Slide 14 text

// junit.framework.ComparisonFailure.java public class ComparisonFailure extends AssertionFailedError { private String fExpected; private String fActual; public ComparisonFailure (String message, String expected, String actual) { super (message); fExpected = expected; fActual = actual; } } private String fExpected; private String fActual; fExpected = expected; fActual = actual; Use-Site Coupling Today's tooling depends on JUnit Internals

Slide 15

Slide 15 text

// junit.framework.ComparisonFailure.java public class ComparisonFailure extends AssertionFailedError { private String expected; private String actual; public ComparisonFailure (String message, String expected, String actual) { super (message); this.expected = expected; this.actual = actual; } } private String expected; private String actual; this.expected = expected; this.actual = actual; Use-Site Coupling Today's tooling depends on JUnit Internals

Slide 16

Slide 16 text

// junit.framework.ComparisonFailure.java public class ComparisonFailure extends AssertionFailedError { private String expected; private String actual; public ComparisonFailure (String message, String expected, String actual) { super (message); this.expected = expected; this.actual = actual; } } Use-Site Coupling Today's tooling depends on JUnit Internals

Slide 17

Slide 17 text

// junit.framework.ComparisonFailure.java public class ComparisonFailure extends AssertionFailedError { private String expected; private String actual; public ComparisonFailure (String message, String expected, String actual) { super (message); this.expected = expected; this.actual = actual; } } // Compile local JUnit $ mvn install ... Use-Site Coupling Today's tooling depends on JUnit Internals

Slide 18

Slide 18 text

class FooTest { @Test fun someTest() { assertEquals(5, 2 + 2) } } Use-Site Coupling Today's tooling depends on JUnit Internals

Slide 19

Slide 19 text

class FooTest { @Test fun someTest() { assertEquals(5, 2 + 2) } } java.lang.AssertionError: expected: but was: Expected :null Actual :null Use-Site Coupling Today's tooling depends on JUnit Internals

Slide 20

Slide 20 text

Two types of limitation inside the current JUnit 4 codebase Use-Site Coupling Ambiguous Evolution

Slide 21

Slide 21 text

Ambiguous Evolution Conflicting requirements reduced clarity

Slide 22

Slide 22 text

@RunWith(Parameterized::class) class BarTest { // ... } Ambiguous Evolution Conflicting requirements reduced clarity

Slide 23

Slide 23 text

@RunWith(Parameterized::class) class BarTest { @Mock lateinit var user: User // ... } @RunWith(Parameterized::class) class BarTest { @Mock lateinit var user: User // ... } Ambiguous Evolution Conflicting requirements reduced clarity

Slide 24

Slide 24 text

@RunWith(MockitoJUnitRunner::class) @RunWith(Parameterized::class) class BarTest { @Mock lateinit var user: User // ... } @RunWith(MockitoJUnitRunner::class) @RunWith(Parameterized::class) class BarTest { } Ambiguous Evolution Conflicting requirements reduced clarity

Slide 25

Slide 25 text

// @RunWith(MockitoJUnitRunner::class) @RunWith(Parameterized::class) class BarTest { @Mock lateinit var user: User @Rule val mockitoRule = MockitoJUnitRule.rule() // ... } // @RunWith(MockitoJUnitRunner::class) @RunWith(Parameterized::class) class BarTest { @Rule val mockitoRule = MockitoJUnitRule.rule() } Ambiguous Evolution Conflicting requirements reduced clarity

Slide 26

Slide 26 text

// @RunWith(MockitoJUnitRunner::class) @RunWith(Parameterized::class) class BarTest { @Mock lateinit var user: User @Rule val mockitoRule = MockitoJUnitRule.rule() // ... } Runner vs Rule: Two concepts for test extension Ambiguous Evolution Conflicting requirements reduced clarity

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

JUnit 5 = Platform + Jupiter + Vintage

Slide 29

Slide 29 text

JUnit 5 = Platform + Jupiter + Vintage junit-platform-engine junit-platform-launcher

Slide 30

Slide 30 text

Platform • Dedicated access point for plugin & build tools developers • TestEngine interface for implementors
 (e.g. Spock, Spek, Cucumber) • Launcher API to kick off test runs

Slide 31

Slide 31 text

JUnit 5 = Platform + Jupiter + Vintage junit-jupiter-api junit-jupiter-engine junit-platform-engine junit-platform-launcher

Slide 32

Slide 32 text

Jupiter • New Programming Model for JUnit tests • Annotations & Extension Point API • JupiterTestEngine

Slide 33

Slide 33 text

JUnit 5 = Platform + Jupiter + Vintage junit-jupiter-api junit-jupiter-engine junit-vintage-engine junit-platform-engine junit-platform-launcher

Slide 34

Slide 34 text

Vintage • Backwards Compatibility layer • Execution of JUnit 3 & 4 @Test classes

Slide 35

Slide 35 text

Your tests junit-jupiter-api junit-4.12 junit-vintage-engine junit-jupiter-engine junit-platform-engine junit-platform-launcher Android Studio, Gradle, etc. written with uses finds & executes implements discovers & runs

Slide 36

Slide 36 text

API Overview

Slide 37

Slide 37 text

JUnit 4 JUnit Jupiter @org.junit.Test → @org.junit.jupiter.api.Test @Ignore → @Disabled @Category(Class) → @Tag(String) Comparisons with JUnit 4: Basic Annotations

Slide 38

Slide 38 text

Comparisons with JUnit 4: Lifecycle Annotations JUnit 4 JUnit Jupiter @BeforeClass → @BeforeAll @Before → @BeforeEach @After → @AfterEach @AfterClass → @AfterAll

Slide 39

Slide 39 text

JUnit Jupiter @Nested @TestFactory @ParameterizedTest & @ExtendWith(Extension) Comparisons with JUnit 4: New Annotations @DisplayName(String)

Slide 40

Slide 40 text

Comparisons with JUnit 4: New Annotations @DisplayName(String) class FooTest { @Test fun checkIfSomethingWorksEvenIfItShouldnt() { // ... } }

Slide 41

Slide 41 text

Comparisons with JUnit 4: New Annotations @DisplayName(String) class FooTest { @Test fun checkIfSomethingWorksEvenIfItShouldnt() { // ... } }

Slide 42

Slide 42 text

Comparisons with JUnit 4: New Annotations @DisplayName(String) class FooTest { @DisplayName("Something works, even if it shouldn't") @Test fun checkIfSomethingWorksEvenIfItShouldnt() { // ... } }

Slide 43

Slide 43 text

Comparisons with JUnit 4: New Annotations @DisplayName(String) class FooTest { @DisplayName("Something works, even if it shouldn't") @Test fun checkIfSomethingWorksEvenIfItShouldnt() { // ... } }

Slide 44

Slide 44 text

Comparisons with JUnit 4: New Annotations @DisplayName(String) @DisplayName("Unexpected Non-Issue Tests") class FooTest { @DisplayName("Something works, even if it shouldn't") @Test fun checkIfSomethingWorksEvenIfItShouldnt() { // ... } }

Slide 45

Slide 45 text

Comparisons with JUnit 4: New Annotations @DisplayName(String) @DisplayName("Unexpected Non-Issue Tests") class FooTest { @DisplayName("Something works, even if it shouldn't") @Test fun checkIfSomethingWorksEvenIfItShouldnt() { // ... } }

Slide 46

Slide 46 text

JUnit Jupiter @DisplayName(String) @TestFactory @ParameterizedTest & @ExtendWith(Extension) Comparisons with JUnit 4: New Annotations @Nested

Slide 47

Slide 47 text

@Nested Comparisons with JUnit 4: New Annotations @DisplayName("Given a Calculator") class CalculatorTests { }

Slide 48

Slide 48 text

@Nested Comparisons with JUnit 4: New Annotations @DisplayName("Given a Calculator") class CalculatorTests { lateinit var calculator: Calculator @BeforeEach fun beforeEach() { this.calculator = Calculator() } }

Slide 49

Slide 49 text

@Nested Comparisons with JUnit 4: New Annotations @DisplayName("Given a Calculator") class CalculatorTests { lateinit var calculator: Calculator @BeforeEach fun beforeEach() { this.calculator = Calculator() } @Nested @DisplayName("When using 'Plus' Operator") inner class PlusOperator { } }

Slide 50

Slide 50 text

@Nested Comparisons with JUnit 4: New Annotations @DisplayName("Given a Calculator") class CalculatorTests { lateinit var calculator: Calculator @BeforeEach fun beforeEach() { this.calculator = Calculator() } @Nested @DisplayName("When using 'Plus' Operator") inner class PlusOperator { @Test @DisplayName("Then Computing 1 and 2 equals 3") fun oneAndTwoIsThree() { val actual = calculator.compute(1, 2, PLUS) Assertions.assertEquals(3, actual) } } }

Slide 51

Slide 51 text

@Nested Comparisons with JUnit 4: New Annotations @DisplayName("Given a Calculator") class CalculatorTests { lateinit var calculator: Calculator @BeforeEach fun beforeEach() { this.calculator = Calculator() } @Nested @DisplayName("When using 'Plus' Operator") inner class PlusOperator { @Test @DisplayName("Then Computing 1 and 2 equals 3") fun oneAndTwoIsThree() { val actual = calculator.compute(1, 2, PLUS) Assertions.assertEquals(3, actual) } } }

Slide 52

Slide 52 text

JUnit Jupiter @DisplayName(String) @Nested @ParameterizedTest & @ExtendWith(Extension) Comparisons with JUnit 4: New Annotations @TestFactory

Slide 53

Slide 53 text

@TestFactory Comparisons with JUnit 4: New Annotations class FooTest { @TestFactory fun createDynamicTests(): List = listOf( dynamicTest("A Dynamic Test") { assertEquals(4, 2 + 2) }, dynamicTest("Another One") { assertEquals(0, 2 - 2) } ) }

Slide 54

Slide 54 text

@TestFactory Comparisons with JUnit 4: New Annotations class FooTest { @TestFactory fun createDynamicTests(): List = listOf( dynamicTest("A Dynamic Test") { assertEquals(4, 2 + 2) }, dynamicTest("Another One") { assertEquals(0, 2 - 2) } ) } @TestFactory fun createDynamicTests(): List

Slide 55

Slide 55 text

@TestFactory Comparisons with JUnit 4: New Annotations class FooTest { @TestFactory fun createDynamicTests(): List = listOf( dynamicTest("A Dynamic Test") { assertEquals(4, 2 + 2) }, dynamicTest("Another One") { assertEquals(0, 2 - 2) } ) } dynamicTest("A Dynamic Test") { assertEquals(4, 2 + 2) }

Slide 56

Slide 56 text

@TestFactory Comparisons with JUnit 4: New Annotations class FooTest { @TestFactory fun createDynamicTests(): List = listOf( dynamicTest("A Dynamic Test") { assertEquals(4, 2 + 2) }, dynamicTest("Another One") { assertEquals(0, 2 - 2) } ) } dynamicTest("A Dynamic Test") { assertEquals(4, 2 + 2) } Factory method for dynamic tests: fun dynamicTest(String, () -> Unit)

Slide 57

Slide 57 text

@TestFactory Comparisons with JUnit 4: New Annotations class FooTest { @TestFactory fun createDynamicTests(): List = listOf( dynamicTest("A Dynamic Test") { assertEquals(4, 2 + 2) }, dynamicTest("Another One") { assertEquals(0, 2 - 2) } ) }

Slide 58

Slide 58 text

@TestFactory Comparisons with JUnit 4: New Annotations class FooTest { @TestFactory fun createDynamicTests(): List = listOf( dynamicTest("A Dynamic Test") { assertEquals(4, 2 + 2) }, dynamicTest("Another One") { assertEquals(0, 2 - 2) } ) }

Slide 59

Slide 59 text

@TestFactory Comparisons with JUnit 4: New Annotations class FooTest { @TestFactory fun createDynamicTests(): List = listOf( dynamicTest("A Dynamic Test") { assertEquals(4, 2 + 2) }, dynamicTest("Another One") { assertEquals(0, 2 - 2) } ) } • Tests generated at runtime • Individual DynamicTest objects do not partake in the lifecycle of a test case (@BeforeEach etc.) • Generally, prefer @ParameterizedTest

Slide 60

Slide 60 text

JUnit Jupiter @DisplayName(String) @Nested @TestFactory @ExtendWith(Extension) Comparisons with JUnit 4: New Annotations @ParameterizedTest &

Slide 61

Slide 61 text

@ParameterizedTest & Comparisons with JUnit 4: New Annotations class FooTest { @ParameterizedTest @CsvSource("Alice, 5", "Bob, 3", "Charles, 7") fun expectedNameLengths(name: String, expectedLen: Int) { Assertions.assertEquals(expectedLen, name.length) } }

Slide 62

Slide 62 text

@ParameterizedTest & Comparisons with JUnit 4: New Annotations class FooTest { @ParameterizedTest @CsvSource("Alice, 5", "Bob, 3", "Charles, 7") fun expectedNameLengths(name: String, expectedLen: Int) { Assertions.assertEquals(expectedLen, name.length) } } @ParameterizedTest

Slide 63

Slide 63 text

@ParameterizedTest & Comparisons with JUnit 4: New Annotations class FooTest { @ParameterizedTest @CsvSource("Alice, 5", "Bob, 3", "Charles, 7") fun expectedNameLengths(name: String, expectedLen: Int) { Assertions.assertEquals(expectedLen, name.length) } } @CsvSource("Alice, 5", "Bob, 3", "Charles, 7") fun expectedNameLengths(name: String, expectedLen: Int)

Slide 64

Slide 64 text

@ParameterizedTest & Comparisons with JUnit 4: New Annotations class FooTest { @ParameterizedTest @CsvSource("Alice, 5", "Bob, 3", "Charles, 7") fun expectedNameLengths(name: String, expectedLen: Int) { Assertions.assertEquals(expectedLen, name.length) } }

Slide 65

Slide 65 text

@ParameterizedTest & Comparisons with JUnit 4: New Annotations class FooTest { @ParameterizedTest @CsvSource("Alice, 5", "Bob, 3", "Charles, 7") fun expectedNameLengths(name: String, expectedLen: Int) { Assertions.assertEquals(expectedLen, name.length) } }

Slide 66

Slide 66 text

Comparisons with JUnit 4: New Annotations class FooTest { @ParameterizedTest @CsvSource("Alice, 5", "Bob, 3", "Charles, 7") fun expectedNameLengths(name: String, expectedLen: Int) { Assertions.assertEquals(expectedLen, name.length) } } • Different possibilities for parameter providers: •@CsvSource •@CsvFileSource •@EnumSource •@MethodSource •@ValueSource • …or implement your own @ParameterizedTest &

Slide 67

Slide 67 text

JUnit Jupiter @DisplayName(String) @Nested @TestFactory @ParameterizedTest & Comparisons with JUnit 4: New Annotations @ExtendWith(Extension)

Slide 68

Slide 68 text

Comparisons with JUnit 4: New Annotations @ExtendWith(Extension) • Unified API for Test Extension • Replacement for @RunWith & @Rule • Repeatable annotation → allows composition • Different interfaces provide hooks into the system • Usage Example: Integration with existing libraries • Complex topic deserving of its own talk

Slide 69

Slide 69 text

No content

Slide 70

Slide 70 text

• JUnit 5 Gradle Plugin only works in pure-Java projects • mannodermaus/android-junit5 • Unit Test Integration for Android projects

Slide 71

Slide 71 text

buildscript { dependencies { classpath "de.mannodermaus.gradle.plugins:android-junit5:1.0.30" } } apply plugin: "de.mannodermaus.android-junit5" dependencies { testImplementation junit5.unitTests() }

Slide 72

Slide 72 text

buildscript { dependencies { classpath "de.mannodermaus.gradle.plugins:android-junit5:1.0.30" } } apply plugin: "de.mannodermaus.android-junit5" dependencies { testImplementation junit5.unitTests() } dependencies { testImplementation junit5.unitTests() } Bundled dependencies for easy setup

Slide 73

Slide 73 text

• New: Experimental Instrumentation Test Support • Successor to JUnit 4 ActivityTestRule • API 26+ • Full Android migration is going to take a while longer…

Slide 74

Slide 74 text

android.testOptions { junitPlatform { instrumentationTests.enabled true } } dependencies { androidTestImplementation junit5.instrumentationTests() }

Slide 75

Slide 75 text

@RunWith(AndroidJUnit4::class) class MyActivityTest { @Rule val rule = ActivityTestRule(MyActivity::class.java) @Test fun testSomething() { rule.launchActivity(null) onView(withId(R.id.textView)).check(...) rule.finishActivity() } } Espresso Test with JUnit 4

Slide 76

Slide 76 text

@ActivityTest(MyActivity::class) class MyActivityTest { @Test fun testSomething(tested: Tested) { tested.launchActivity() onView(withId(R.id.textView)).check(...) tested.finishActivity() } } Espresso Test with JUnit 5

Slide 77

Slide 77 text

@ActivityTest(MyActivity::class) class MyActivityTest { @Test fun testSomething(tested: Tested) { tested.launchActivity() onView(withId(R.id.textView)).check(...) tested.finishActivity() } } Espresso Test with JUnit 5 @ActivityTest(MyActivity::class)

Slide 78

Slide 78 text

@ActivityTest(MyActivity::class) class MyActivityTest { @Test fun testSomething(tested: Tested) { tested.launchActivity() onView(withId(R.id.textView)).check(...) tested.finishActivity() } } Espresso Test with JUnit 5 @ActivityTest(MyActivity::class) Test Extension with custom Configuration Parameters: •targetPackage •launchFlags •launchActivity •…

Slide 79

Slide 79 text

@ActivityTest(MyActivity::class) class MyActivityTest { @Test fun testSomething(tested: Tested) { tested.launchActivity() onView(withId(R.id.textView)).check(...) tested.finishActivity() } } Espresso Test with JUnit 5 fun testSomething(tested: Tested)

Slide 80

Slide 80 text

@ActivityTest(MyActivity::class) class MyActivityTest { @Test fun testSomething(tested: Tested) { tested.launchActivity() onView(withId(R.id.textView)).check(...) tested.finishActivity() } } Espresso Test with JUnit 5 fun testSomething(tested: Tested) Access to Activity under test, successor to old ActivityTestRule: •Tested#launchActivity •Tested#finishActivity •Tested#getActivityResult

Slide 81

Slide 81 text

@ActivityTest(MyActivity::class) class MyActivityTest { @Test fun testSomething(tested: Tested) { tested.launchActivity() onView(withId(R.id.textView)).check(...) tested.finishActivity() } } Espresso Test with JUnit 5 @ActivityTest(MyActivity::class) class MyActivityTest { @Test fun testSomething(tested: Tested) { tested.launchActivity() onView(withId(R.id.textView)).check(...) tested.finishActivity() } }

Slide 82

Slide 82 text

The future is bright for Testing.

Slide 83

Slide 83 text

It'll take some more time before JUnit 5 becomes common practice in Android.

Slide 84

Slide 84 text

Tools today already make it possible to become an early adopter.

Slide 85

Slide 85 text

https://github.com/mannodermaus/android-junit5

Slide 86

Slide 86 text

https://www.mytenten.com/jobs

Slide 87

Slide 87 text

Moving Forward with JUnit 5