Slide 1

Slide 1 text

Aung Kyaw Paing Senior Consultant @ thoughtworks | GDE Android aungkyawpaing.dev State of Junit 5 in Android

Slide 2

Slide 2 text

1997

Slide 3

Slide 3 text

“JUnit was born on a flight from Zurich to the 1997 OOPSLA in Atlanta. Kent was flying with Erich Gamma, and what else were two geeks to do on a long flight but program?” - Martin Fowler

Slide 4

Slide 4 text

In fact, the original SUnit framework only has only three small classes and twelve methods!

Slide 5

Slide 5 text

“Never in the field of software development have so many owed so much to so few lines of code” - Martin Fowler

Slide 6

Slide 6 text

2006

Slide 7

Slide 7 text

Junit 4 - Annotations were introduced - Method name no longer require you to start with “test” prefix - Runners are introduced

Slide 8

Slide 8 text

2016

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

Runners - Can only have one single runner for each test class - You can’t have Parameterized test if you’re using other class runner like AndroidTestRunner - We want to separate runner, reporting and so on into different interfaces

Slide 11

Slide 11 text

Execution model - Requires all test to be known prior to execution - Prevent dynamic creation of test cases during execution

Slide 12

Slide 12 text

Toolchain - IDEs and build tools are tightly coupled to JUnit internals - Some tools use reflection to access internal APIs - In short, JUnit 4 was not made to be extensible across multiple tools

Slide 13

Slide 13 text

The vision - Decouple test execution and reporting from test definition and provisioning - Rethinking JUnit’s extensibility - Make use of Java 8 features

Slide 14

Slide 14 text

2017 JUnit 5 become stable

Slide 15

Slide 15 text

What’s in JUnit 5 for me?

Slide 16

Slide 16 text

Cleaner Reporting

Slide 17

Slide 17 text

Kotlin Friendly @Test fun `should return invalid when text is empty`() { ... }

Slide 18

Slide 18 text

Multiple Assertions Assertions.assertAll( "Assertions subset of variable", { Assertions.assertEquals(actual.name, "Vincent")}, { Assertions.assertEquals(actual.age, 28)}, { Assertions.assertTrue(actual.isSpeaker)}, )

Slide 19

Slide 19 text

Asserting Exceptions val exception = assertThrows( IOException::class.java ) { functionThatThrow() } assertEquals(exception.message, "Error reading file")

Slide 20

Slide 20 text

Assumptions @Test fun shouldGetFromDatabase() { // Abort test if watch is not connected Assumptions.assumeTrue(isConnectedToWearOS())) // Execute test }

Slide 21

Slide 21 text

Inner Tests class ProfileScreenTest { @InnerTest @DisplayName("when user is logged in") inner class WhenUserLoggedInTests { // ... test functions } @InnerTest @DisplayName("when user is logged out") inner class WhenUserLoggedOutTests { // ... test functions } }

Slide 22

Slide 22 text

Tagging @Test @Tag("Fast") fun fastTestThatCanRunLocally() @Test @Tag("Slow") fun slowTestThatWeRunOnCI()

Slide 23

Slide 23 text

Tagging task slowTest(type: Test) { useJUnitPlatform { includeTags 'slow' } } // .gradlew slowTest

Slide 24

Slide 24 text

Conditional tests @Test @EnabledOnOs({OS.MAC}) fun shouldRunOnlyOnMac() { // ... }

Slide 25

Slide 25 text

Repeated tests @RepeatedTest(value = 3, failureThreshold = 1) fun flakyTests() { // ... }

Slide 26

Slide 26 text

Parameterized tests @ParameterizedTest @ValueSource( strings = ["", "abcd", "abc213"] ) fun validateShouldReturnFalse(input: String) { Assertions.assertFalse(validatePhoneNumber(input)) }

Slide 27

Slide 27 text

Dynamic Tests @TestFactory fun apiContractTests(): List { val apiDataFile : List = readAssetDir("api_tests") apiDataFile.map { metadata -> dynamicTest("API Test: ${metadata.endpoint}") { // PING API } } }

Slide 28

Slide 28 text

JUnit 4 Backwards compatibility

Slide 29

Slide 29 text

class JUnit4Test { @Before fun setUp() { } @After fun tearDown() {} @Test fun test() { Assert.assertEquals(true, true) } } Similar structures class JUnit5Test { @BeforeEach fun setUp() { } @AfterEach fun tearDown() {} @Test fun test() { Assertions.assertEquals(true, true) } }

Slide 30

Slide 30 text

@get:Rule val composeTestRule = createComposeRule() Rule —> Extensions

Slide 31

Slide 31 text

Rule —> Extensions @JvmField @RegisterExtension val extension = createComposeExtension()

Slide 32

Slide 32 text

implementation ("org.junit.vintage:junit-vintage-engine") Or just run Junit4!

Slide 33

Slide 33 text

JUnit 5 on Android

Slide 34

Slide 34 text

JUnit 5 on Android - Google has no plan to officially support for Junit 5 as it will require a lot of resources

Slide 35

Slide 35 text

JUnit 5 on Android - Google has no plan to officially support for Junit 5 as it will require a lot of resources - However we have community maintained plugin!

Slide 36

Slide 36 text

plugins { id("de.mannodermaus.android-junit5") version "1.11.2.0" }

Slide 37

Slide 37 text

plugins { id("de.mannodermaus.android-junit5") version "1.11.2.0" } dependencies { // (Required) Writing and executing Unit Tests on the JUnit Platform testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.2") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.2") // (Optional) If you need "Parameterized Tests" testImplementation("org.junit.jupiter:junit-jupiter-params:5.11.2") // (Optional) If you also have JUnit 4-based tests testImplementation("junit:junit:4.13.2") testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.11.2") }

Slide 38

Slide 38 text

Instrumentation Tests dependencies { androidTestImplementation("org.junit.jupiter:junit-jupiter-api:5.11.2") }

Slide 39

Slide 39 text

Instrumentation Tests class MyActivityTest { @JvmField @RegisterExtension val scenarioExtension = ActivityScenarioExtension.launch() @Test fun myTest() { val scenario = scenarioExtension.scenario // Do something with the scenario here... } }

Slide 40

Slide 40 text

Compose Tests dependencies { androidTestImplementation("de.mannodermaus.junit5:android-test-compose") }

Slide 41

Slide 41 text

Compose Tests class ComposeTest { @JvmField @RegisterExtension val extension = createComposeExtension() @Test fun composeTest() = extension.use { setContent { Text("Hello") } onNodeWithText("Hello").assertIsDisplayed() } }

Slide 42

Slide 42 text

Roboletric Android JUnit4 - No official support yet (#3477) - Is part of Google Summer of Code 2024 (Pitch Deck)

Slide 43

Slide 43 text

Roboletric Android JUnit4 - No official support yet (#3477) - Is part of Google Summer of Code 2024 (Pitch Deck) - But we have a community plugin!

Slide 44

Slide 44 text

Roboletric plugins { id("tech.apter.junit5.jupiter.robolectric-extension-gradle-plugin") version "0.8.0" } @ExtendWith(RobolectricExtension::class) class RobolectricTest { @Test fun `JUnit5 with robolectric`() { assertNotNull(RuntimeEnvironment.getApplication()) } }

Slide 45

Slide 45 text

Gradle Managed Device Gradle Managed Device is the recommended way to run instrumentation tests as it interacts with actual system API and it’s fast unlike running on actual emulators or real devices. It is also scalable with Firebase Test Lab!

Slide 46

Slide 46 text

testOptions { managedDevices { localDevices { create("pixel") { // Use device profiles you typically see in Android Studio. device = "Pixel 2" // Use only API levels 27 and higher. apiLevel = 30 // To include Google services, use "google". systemImageSource = "aosp" } } } }

Slide 47

Slide 47 text

Unit Tests Features Supported? Instrumentation Compose Dagger Hilt HiltAndroidRule has no equivalent extension as it requires annotating with HiltAndroidTest to generate code Robolectric Limited support

Slide 48

Slide 48 text

My Recommendations Unit Tests Instrumentation Tests UI Tests E2E Junit 5 Junit 5 running on Gradle Managed Device Maestro, Appium etc

Slide 49

Slide 49 text

So what can you do?

Slide 50

Slide 50 text

What can I do? - +1 in the issue tracker and voice out if you have any https://issuetracker.google.com/issues/127100532 - Thumbs up on Robolectric open issue https://github.com/robolectric/robolectric/issues/3477 - Star android-junit5 library https://github.com/mannodermaus/android-junit5 - Start using Junit5 in your app

Slide 51

Slide 51 text

Aung Kyaw Paing Senior Consultant @ thoughtworks | GDE Android aungkyawpaing.dev State of Junit 5 in Android