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

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() } }