Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Moving Forward with JUnit 5

Moving Forward with JUnit 5

DroidKaigi 2018, Tokyo, Japan

Avatar for Marcel Schnelle

Marcel Schnelle

February 09, 2018
Tweet

More Decks by Marcel Schnelle

Other Decks in Programming

Transcript

  1. Two types of limitation inside the current JUnit 4 codebase

    Use-Site Coupling Ambiguous Evolution
  2. Use-Site Coupling Today's tooling depends on JUnit Internals class FooTest

    { @Test fun someTest() { assertEquals(5, 2 + 2) } }
  3. class FooTest { @Test fun someTest() { assertEquals(5, 2 +

    2) } } java.lang.AssertionError: expected:<5> but was:<4> Expected :5 Actual :4 <Click to see difference> Use-Site Coupling Today's tooling depends on JUnit Internals
  4. // 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
  5. // 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
  6. // 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
  7. // 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
  8. // 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
  9. class FooTest { @Test fun someTest() { assertEquals(5, 2 +

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

    2) } } java.lang.AssertionError: expected:<null> but was:<null> Expected :null Actual :null <Click to see difference> Use-Site Coupling Today's tooling depends on JUnit Internals
  11. Two types of limitation inside the current JUnit 4 codebase

    Use-Site Coupling Ambiguous Evolution
  12. @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
  13. @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
  14. // @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
  15. // @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
  16. 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
  17. Jupiter • New Programming Model for JUnit tests • Annotations

    & Extension Point API • JupiterTestEngine
  18. JUnit 5 = Platform + Jupiter + Vintage junit-jupiter-api junit-jupiter-engine

    junit-vintage-engine junit-platform-engine junit-platform-launcher
  19. Your tests junit-jupiter-api junit-4.12 <other framework> junit-vintage-engine junit-jupiter-engine <other engine>

    junit-platform-engine junit-platform-launcher Android Studio, Gradle, etc. written with uses finds & executes implements discovers & runs
  20. JUnit 4 JUnit Jupiter @org.junit.Test → @org.junit.jupiter.api.Test @Ignore → @Disabled

    @Category(Class) → @Tag(String) Comparisons with JUnit 4: Basic Annotations
  21. Comparisons with JUnit 4: Lifecycle Annotations JUnit 4 JUnit Jupiter

    @BeforeClass → @BeforeAll @Before → @BeforeEach @After → @AfterEach @AfterClass → @AfterAll
  22. Comparisons with JUnit 4: New Annotations @DisplayName(String) class FooTest {

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

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

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

    @DisplayName("Something works, even if it shouldn't") @Test fun checkIfSomethingWorksEvenIfItShouldnt() { // ... } }
  26. 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() { // ... } }
  27. 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() { // ... } }
  28. @Nested Comparisons with JUnit 4: New Annotations @DisplayName("Given a Calculator")

    class CalculatorTests { lateinit var calculator: Calculator @BeforeEach fun beforeEach() { this.calculator = Calculator() } }
  29. @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 { } }
  30. @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) } } }
  31. @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) } } }
  32. @TestFactory Comparisons with JUnit 4: New Annotations class FooTest {

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

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

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

    @TestFactory fun createDynamicTests(): List<DynamicTest> = 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)
  36. @TestFactory Comparisons with JUnit 4: New Annotations class FooTest {

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

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

    @TestFactory fun createDynamicTests(): List<DynamicTest> = 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
  39. @ParameterizedTest & <Source> 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) } }
  40. @ParameterizedTest & <Source> 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
  41. @ParameterizedTest & <Source> 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)
  42. @ParameterizedTest & <Source> 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) } }
  43. @ParameterizedTest & <Source> 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) } }
  44. 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 & <Source>
  45. 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
  46. • JUnit 5 Gradle Plugin only works in pure-Java projects

    • mannodermaus/android-junit5 • Unit Test Integration for Android projects
  47. buildscript { dependencies { classpath "de.mannodermaus.gradle.plugins:android-junit5:1.0.30" } } apply plugin:

    "de.mannodermaus.android-junit5" dependencies { testImplementation junit5.unitTests() }
  48. 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
  49. • New: Experimental Instrumentation Test Support • Successor to JUnit

    4 ActivityTestRule • API 26+ • Full Android migration is going to take a while longer…
  50. android.testOptions { junitPlatform { instrumentationTests.enabled true } } dependencies {

    androidTestImplementation junit5.instrumentationTests() }
  51. @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
  52. @ActivityTest(MyActivity::class) class MyActivityTest { @Test fun testSomething(tested: Tested<MyActivity>) { tested.launchActivity()

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

    onView(withId(R.id.textView)).check(...) tested.finishActivity() } } Espresso Test with JUnit 5 @ActivityTest(MyActivity::class)
  54. @ActivityTest(MyActivity::class) class MyActivityTest { @Test fun testSomething(tested: Tested<MyActivity>) { 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 •…
  55. @ActivityTest(MyActivity::class) class MyActivityTest { @Test fun testSomething(tested: Tested<MyActivity>) { tested.launchActivity()

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

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