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

Berlindroid: Kotlin Test Robots

Ash Davies
September 28, 2017

Berlindroid: Kotlin Test Robots

Following the announcement at this year's Google IO we've seen a dramatic uptake in Kotlin and over the last few months we've learnt how we can use it to improve and produce clean architecture applications and supercharge our unit tests, but what about our UI tests?

In this session, I'll show you how you can utilise Kotlin with the Robot pattern to build concise, terse and easy to read UI tests

Ash Davies

September 28, 2017
Tweet

More Decks by Ash Davies

Other Decks in Programming

Transcript

  1. Kotlin Test Robots
    ashdavies.io

    View Slide

  2. Testing
    ashdavies.io

    View Slide

  3. Tooling
    ashdavies.io

    View Slide

  4. ashdavies.io

    View Slide

  5. UI Testing
    ashdavies.io

    View Slide

  6. Architecture Agnostic
    ashdavies.io

    View Slide

  7. Espresso
    ashdavies.io

    View Slide

  8. ashdavies.io

    View Slide

  9. ViewInteraction appCompatEditText = onView(
    allOf(withId(R.id.email),
    withParent(allOf(withId(R.id.activity_main),
    withParent(withId(android.R.id.content)))),
    isDisplayed()));
    appCompatEditText.perform(click());
    ViewInteraction appCompatEditText2 = onView(
    allOf(withId(R.id.email),
    withParent(allOf(withId(R.id.activity_main),
    withParent(withId(android.R.id.content)))),
    isDisplayed()));
    appCompatEditText2.perform(replaceText("test@invalid"), closeSoftKeyboard());
    ViewInteraction appCompatButton = onView(
    allOf(withId(R.id.validate_button), withText("Validate"),
    withParent(allOf(withId(R.id.activity_main),
    withParent(withId(android.R.id.content)))),
    isDisplayed()));
    appCompatButton.perform(click());
    ViewInteraction textView = onView(
    allOf(withId(R.id.validation_result), withText("Invalid Email Format"),
    childAtPosition(
    allOf(withId(R.id.activity_main),
    childAtPosition(
    withId(android.R.id.content),
    0)),
    1),
    isDisplayed()));
    textView.check(matches(isDisplayed()));
    ashdavies.io

    View Slide

  10. "I've calculated your chance of survival,
    but I don't think you'll like it."
    -- Marvin
    ashdavies.io

    View Slide

  11. Espresso
    ashdavies.io

    View Slide

  12. Espresso
    @Test
    fun shouldValidateEmailAddress() {
    onView(withId(R.id.email)).check(matches(isDisplayed()))
    onView(withId(R.id.email)).perform(typeText("ash"))
    onView(withId(R.id.login)).perform(click())
    onView(withId(R.id.error)).check(matches(allOf(isDisplayed(), withText(R.string.invalid_email))))
    }
    ashdavies.io

    View Slide

  13. Espresso
    @Test
    fun `should validate email address`() {
    onView(withId(R.id.email)).check(matches(isDisplayed()))
    onView(withId(R.id.email)).perform(typeText("ash"))
    onView(withId(R.id.login)).perform(click())
    onView(withId(R.id.error)).check(matches(allOf(isDisplayed(), withText(R.string.invalid_email))))
    }
    ashdavies.io

    View Slide

  14. ashdavies.io

    View Slide

  15. Robots
    ashdavies.io

    View Slide

  16. ashdavies.io

    View Slide

  17. Robots
    ashdavies.io

    View Slide

  18. Robots
    — Structure your own domain language
    ashdavies.io

    View Slide

  19. Robots
    — Structure your own domain language
    — Contextualise assertions and verifications
    ashdavies.io

    View Slide

  20. Robots
    — Structure your own domain language
    — Contextualise assertions and verifications
    — High level intention language
    ashdavies.io

    View Slide

  21. Robots
    — Structure your own domain language
    — Contextualise assertions and verifications
    — High level intention language
    — Reads like a script
    ashdavies.io

    View Slide

  22. Sound Familiar?
    Testing Robots (Kotlin Night) - JW
    ashdavies.io

    View Slide

  23. ashdavies.io

    View Slide

  24. ashdavies.io

    View Slide

  25. Robot
    class FinanceRobot {
    fun price(amount: Int): FinanceRobot { ... }
    fun costs(amount: Int): FinanceRobot { ... }
    fun funds(amount: Int): FinanceRobot { ... }
    fun next(): ResultRobot { ... }
    }
    ashdavies.io

    View Slide

  26. Robots
    class FinanceRobot {
    fun price(amount: Int) = apply {
    onChild(withId(R.id.price)).perform(typeText(value))
    }
    fun costs(amount: Int) = apply {
    onChild(withId(R.id.costs)).perform(typeText(value))
    }
    fun funds(amount: Int) = apply {
    onChild(withId(R.id.funds)).perform(typeText(value))
    }
    fun next() = apply {
    onChild(withId(R.id.next)).perform(click())
    }
    }
    ashdavies.io

    View Slide

  27. class FinanceTest {
    fun `should update seek bar amount`() {
    FinanceRobot()
    .price(200_000)
    .next()
    }
    }
    ashdavies.io

    View Slide

  28. Cucumber
    Gherkin
    Feature: Some terse yet descriptive text of what is desired
    Textual description of the business value of this feature
    Business rules that govern the scope of the feature
    Any additional information that will make the feature easier to understand
    Scenario: Some determinable business situation
    Given some precondition
    And some other precondition
    When some action by the actor
    And some other action
    And yet another action
    Then some testable outcome is achieved
    And something else we can check happens too
    ashdavies.io

    View Slide

  29. Kotlin
    @Test
    fun `should update progress bar amount`() {
    FinanceRobot()
    .given { amount(200_000) }
    .then { progress(200_000) }
    }
    ashdavies.io

    View Slide

  30. Kotlin
    class FinanceRobot {
    fun given(func: Given.() -> Unit) = Given().apply(func)
    class Given {
    fun amount(value: Int) = apply {
    onChild(withId(R.id.input)).perform(typeText(value))
    }
    }
    }
    ashdavies.io

    View Slide

  31. Kotlin
    class FinanceRobot {
    fun given(func: Given.() -> Unit) = Given().apply(func)
    class Given {
    fun amount(value: Int) = apply {
    onChild(withId(R.id.input)).perform(typeText(value))
    }
    }
    }
    ashdavies.io

    View Slide

  32. Kotlin
    class FinanceRobot {
    fun given(func: Given.() -> Unit) = Given().apply(func)
    class Given {
    fun amount(value: Int) = apply {
    onChild(withId(R.id.input)).perform(typeText(value))
    }
    }
    }
    ashdavies.io

    View Slide

  33. Kotlin
    class FinanceRobot {
    fun given(func: Given.() -> Unit) = Given().apply(func)
    class Given {
    fun amount(value: Int) = apply {
    onChild(withId(R.id.input)).perform(typeText(value))
    }
    fun then(func: Then.() -> Unit) = Then().apply(func)
    }
    }
    ashdavies.io

    View Slide

  34. Kotlin
    class FinanceRobot {
    fun given(func: Given.() -> Unit) = Given().apply(func)
    class Given {
    fun inputText(value: Int) = apply {
    onChild(withId(R.id.input)).perform(typeText(value))
    }
    fun then(func: Then.() -> Unit) = Then().apply(func)
    }
    class Then {
    fun hasProgress(value: Int) = apply {
    onChild(withId(R.id.progress)).perform(scrubProgress(value))
    }
    }
    }
    ashdavies.io

    View Slide

  35. IntentsTestRule
    @get:Rule
    val rule: IntentsTestRule = FinanceRobot.rule(FinanceActivity::class.java, false, false)
    ashdavies.io

    View Slide

  36. Robot Companion
    class FinanceRobot {
    companion object {
    fun start(rule: IntentsTestRule, amount: Int = 200_000): FinanceRobot {
    rule.launchActivity(FinanceActivity.newIntent(context, amount))
    return FinanceRobot()
    }
    }
    }
    ashdavies.io

    View Slide

  37. Testing Robots
    @Test
    fun `should update progress bar amount`() {
    FinanceRobot.start(rule)
    given { inputText("200000") }
    then { hasProgress(200_000) }
    }
    ashdavies.io

    View Slide

  38. Testing Robots
    @Test
    fun `should update progress bar amount`() {
    FinanceRobot.start(rule)
    given { /* ??? */ }
    then { /* ??? */ }
    }
    ashdavies.io

    View Slide

  39. Kotlin
    class FinanceRobot {
    fun given(func: Given.() -> Unit) = Given().apply(func)
    class Given {
    fun inputText(value: Int) = apply { /* ... */ }
    fun then(func: Then.() -> Unit) = Then().apply(func)
    }
    class Then {
    fun hasProgress(value: Int) = apply { /* ... */ }
    }
    }
    ashdavies.io

    View Slide

  40. class FinanceRobot {
    fun given(func: Given.() -> Unit) = Given().apply(func)
    class Given {
    fun price(func: Will.() -> Unit) = Will(R.id.price).apply(func)
    class Will(@IdRes parent: Int) {
    fun inputText(value: Int) = apply { /* ... */ }
    fun then(func: Then.() -> Unit) = Then().apply(func)
    }
    }
    class Then {
    fun hasProgress(value: Int) = apply { /* ... */ }
    }
    }
    ashdavies.io

    View Slide

  41. class FinanceRobot {
    fun given(func: Given.() -> Unit) = Given().apply(func)
    class Given {
    fun price(func: Will.() -> Unit) = Will(R.id.price).apply(func)
    class Will(@IdRes parent: Int) {
    fun inputText(value: Int) = apply {
    onView(allOf(isDescendantOfA(withId(parent)), withId(R.id.input)))
    .perform(typeText(value)).perform(pressImeActionButton())
    }
    fun then(func: Then.() -> Unit) = Then().apply(func)
    }
    }
    class Then {
    fun hasProgress(value: Int) = apply { /* ... */ }
    }
    }
    ashdavies.io

    View Slide

  42. class FinanceRobot {
    fun given(func: Given.() -> Unit) = Given().apply(func)
    class Given {
    fun price(func: Will.() -> Unit) = {
    Will(R.id.price).apply(func)
    return FinanceRobot()
    }
    class Will(@IdRes parent: Int) {
    fun inputText(value: Int) = apply { /* ... */ }
    }
    }
    fun then(func: Then.() -> Unit) = Then().apply(func)
    class Then {
    fun hasProgress(value: Int) = apply { /* ... */ }
    }
    }
    ashdavies.io

    View Slide

  43. Testing Robots
    fun `should update progress bar amount`() {
    FinanceRobot.start(rule)
    given {
    price { inputText("200000") }
    funds { scrubTo(40_000) }
    }
    then {
    price { hasText("200.000 €") }
    funds { hasText("40.000 € / 20 %") }
    }
    }
    ashdavies.io

    View Slide

  44. Testing Robots
    fun `should update progress bar amount`() {
    rule.start()
    given {
    price { inputText("200000") }
    funds { scrubTo(40_000) }
    }
    then {
    price { hasText("200.000 €") }
    funds { hasText("40.000 € / 20 %") }
    }
    }
    fun IntentsTestRule.start(amount: Int = 200_000): FinanceRobot {
    return FinanceRobot.start(this, amount)
    }
    ashdavies.io

    View Slide

  45. Thanks
    ashdavies.io

    View Slide