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

Drink Espresso During Android Testing - Introduction

Alex Zhukovich
September 18, 2017

Drink Espresso During Android Testing - Introduction

Mobile apps are growing. They become more complex and require more testing. It means that it is time to integrate automating tests to your project. In this talk we will discuss shortly testing of Android project in general and User Interface testing with Espresso framework in detail.

Alex Zhukovich

September 18, 2017
Tweet

More Decks by Alex Zhukovich

Other Decks in Technology

Transcript

  1. View Slide

  2. Welcome to Android World!

    View Slide

  3. Application is growing

    View Slide

  4. Application is growing

    View Slide

  5. Categories of Android tests
    Run on the JVM
    (local tests)
    Require Android
    device
    (instrumentation tests)
    Tests

    View Slide

  6. How to test the project?

    View Slide

  7. Unit tests
    JUnit
    Hamcrest
    Mockito
    Robolectric

    View Slide

  8. Integration tests
    JUnit
    Hamcrest
    Mockito
    Instrumentation
    API

    View Slide

  9. Acceptance tests
    Espresso
    UiAutomator
    Robotium
    Calabash
    Appium

    View Slide

  10. Android Testing Support Library
    AndroidJUnitRunner
    JUnit4 Rules
    Espresso
    UiAutomator

    View Slide

  11. AndroidJUnitRunner
    ü JUnit4 support
    ü Instrumentation Registry
    ü Activity and Application life-cycle monitoring

    View Slide

  12. JUnit4 Rules
    ü ActivityTestRule
    ü IntentsTestRule
    ü ServiceTestRule
    ü ProviderTestRule
    ü GrantPermissionRule (API 23 – Android Marshmallow)

    View Slide

  13. Espresso
    Android 2.2 (API 8)
    Turn off the animations

    View Slide

  14. Espresso
    onView(ViewMatcher)
    .perform(ViewAction)
    .check(ViewAssertion)

    View Slide

  15. Get View by text
    Matcher
    Action
    Assertion
    onView(withText("Pay"))

    View Slide

  16. Get View by text
    Matcher
    Action
    Assertion
    onView(withText(R.string.pay))

    View Slide

  17. Get View by id
    Matcher
    Action
    Assertion
    onView(withId(R.id.pay))

    View Slide

  18. Click to view
    Matcher
    Action
    Assertion
    onView(withId(R.id.pay))
    .perform(click())

    View Slide

  19. Long click to view
    Matcher
    Action
    Assertion
    onView(withId(R.id.pay))
    .perform(longClick())

    View Slide

  20. Double click to view
    Matcher
    Action
    Assertion
    onView(withId(R.id.pay))
    .perform(doubleClick())

    View Slide

  21. Enter a text
    Matcher
    Action
    Assertion
    onView(withId(R.id.payment))
    .perform(typeText("120$"))

    View Slide

  22. Close a keyboard
    Matcher
    Action
    Assertion
    onView(withId(R.id.payment))
    .perform(closeSoftKeyboard())

    View Slide

  23. Enter a text, close a keyboard
    Matcher
    Action
    Assertion
    onView(withId(R.id.payment))
    .perform(typeText("120$"),
    closeSoftKeyboard())

    View Slide

  24. Check the hint
    Matcher
    Action
    Assertion
    onView(withId(R.id.payment))
    .check(matches(
    withHint(R.string.payment_hint)
    ))

    View Slide

  25. Check the text
    Matcher
    Action
    Assertion
    onView(withId(R.id.payment))
    .check(matches(
    withText(R.string.payment)))

    View Slide

  26. Check the visibility
    Matcher
    Action
    Assertion
    onView(withId(R.id.payment))
    .check(matches(
    isDisplayed()))

    View Slide

  27. onView(withId(R.id.payment))
    .perform(typeText("120$"),
    closeSoftKeyboard())
    onView(withId(R.id.pay_button))
    .check(matches(isDisplayed()))
    .perform(click())

    View Slide

  28. onView(withId(R.id.payment))
    .perform(typeText("120$"),
    closeSoftKeyboard())
    onView(withId(R.id.pay_button))
    .check(matches(isDisplayed()))
    .perform(click())

    View Slide

  29. Hamcrest matchers

    View Slide

  30. onView(withId(R.id.payment))
    .check(matches(
    `is`
    withText(startsWith("Go"))))
    endsWith

    View Slide

  31. @RunWith(AndroidJUnit4::class)
    class LoginActivityTest {
    @Rule @JvmField
    val mActivityRule =
    ActivityTestRule(LoginActivity::class.java)
    @Test
    fun shouldVerifyEmptyPassword() {
    onView(withId(R.id.edit_text_username))
    .perform(typeText("Alex"),
    closeSoftKeyboard())
    onView(withId(R.id.login_button))
    .perform(click())
    onView(withId(R.id.password_text_layout))
    .check(matches(
    withError(`is`("Please enter a password"))))
    }
    }

    View Slide

  32. @RunWith(AndroidJUnit4::class)
    class LoginActivityTest {
    @Rule @JvmField
    val mActivityRule = ActivityTestRule
    (LoginActivity::class.java)
    @Test
    fun shouldVerifyEmptyPassword() {
    onView(withId(R.id.edit_text_username))
    .perform(typeText("Alex"),
    closeSoftKeyboard())
    onView(withId(R.id.login_button))
    .perform(click())
    onView(withId(R.id.password_text_layout))
    .check(matches(
    withError(`is`("Please enter a password"))))
    }
    }

    View Slide

  33. @RunWith(AndroidJUnit4::class)
    class LoginActivityTest {
    @Rule @JvmField
    val mActivityRule = ActivityTestRule(LoginActivity::class.java)
    @Test
    fun shouldVerifyEmptyPassword() {
    onView(withId(R.id.edit_text_username))
    .perform(typeText("Alex"),
    closeSoftKeyboard())
    onView(withId(R.id.login_button))
    .perform(click())
    onView(withId(R.id.password_text_layout))
    .check(matches(
    withError(`is`("Please enter a password"))))
    }
    }

    View Slide

  34. @RunWith(AndroidJUnit4::class)
    class LoginActivityTest {
    @Rule @JvmField
    val mActivityRule = ActivityTestRule(LoginActivity::class.java)
    @Test
    fun shouldVerifyEmptyPassword() {
    onView(withId(R.id.edit_text_username))
    .perform(typeText("Alex"),
    closeSoftKeyboard())
    onView(withId(R.id.login_button))
    .perform(click())
    onView(withId(R.id.password_text_layout))
    .check(matches(
    withError(`is`("Please enter a password"))))
    }
    }

    View Slide

  35. @RunWith(AndroidJUnit4::class)
    class LoginActivityTest {
    @Rule @JvmField
    val mActivityRule = ActivityTestRule(LoginActivity::class.java)
    @Test
    fun shouldVerifyEmptyPassword() {
    onView(withId(R.id.edit_text_username))
    .perform(typeText("Alex"),
    closeSoftKeyboard())
    onView(withId(R.id.login_button))
    .perform(click())
    onView(withId(R.id.password_text_layout))
    .check(matches(
    withError(`is`("Please enter a password"))))
    }
    }

    View Slide

  36. @RunWith(AndroidJUnit4::class)
    class LoginActivityTest {
    @Rule @JvmField
    val mActivityRule = ActivityTestRule(LoginActivity::class.java)
    @Test
    fun shouldVerifyEmptyPassword() {
    onView(withId(R.id.edit_text_username))
    .perform(typeText("Alex"),
    closeSoftKeyboard())
    onView(withId(R.id.login_button))
    .perform(click())
    onView(withId(R.id.password_text_layout))
    .check(matches(withError(`is`(
    "Please enter a password"))))
    }
    }
    * withError – a custom matcher

    View Slide

  37. onView(withId(R.id.download))
    .perform(click())
    onView(withId(android.support.design.R.id.snackbar_text))
    .check(matches(
    withText(R.string.downloading_movie_info)))

    View Slide

  38. What is an AdapterView and how it works?
    java.lang.Object
    ↳ android.view.View
    ↳ android.view.ViewGroup
    ↳ android.widget.AdapterView

    GridView ListView
    Spinner

    View Slide

  39. Espresso
    onData(ObjectMatcher)
    .DataOptions
    .perform(ViewAction)
    .check(ViewAssertion)

    View Slide

  40. onData – ArrayAdapter
    onData(allOf(`is`(instanceOf(String::class.java)),
    `is`("Americano")))
    .check(matches(isDisplayed()))
    val arrayAdapter = ArrayAdapter(
    [email protected],
    android.R.layout.simple_list_item_1,
    android.R.id.text1,
    coffeeList)

    View Slide

  41. onData – custom adapter
    @Test
    fun clickCoffeeItem() {
    ...
    onData(withCoffee(espresso))
    .inAdapterView(withId(R.id.detail_list))
    .perform(click());
    }
    class CoffeeAdapter : BaseAdapter() {
    ...
    override fun getItem(position: Int): Any {
    return mOrderedCoffee.keys.toTypedArray()[position]
    }
    }

    View Slide

  42. fun withCoffee(expected: Coffee) : Matcher {
    return object : BoundedMatcher(Coffee::class.java) {
    override fun describeTo(description: Description) {
    description.appendText("has value: $expected")
    }
    override fun matchesSafely(item: Coffee): Boolean {
    return item == expected
    }
    }
    }

    View Slide

  43. RecyclerView
    java.lang.Object
    ↳ android.view.View
    ↳ android.view.ViewGroup
    ↳ android.support.v7.widget.RecyclerView
    It is not an AdapterView

    View Slide

  44. RecyclerViewActions
    ü actionOnHolderItem
    ü actionOnItem
    ü actionOnItemAtPosition
    ü scrollTo
    ü scrollToHolder
    ü scrollToPosition
    onView(withId(R.id.recyclerView))
    .perform(actionOnItemAtPosition(3, click()));

    View Slide

  45. IdlingResource
    class ServiceIdlingResource : IdlingResource {
    lateinit var mResourceCallback : ResourceCallback
    override fun getName(): String {
    return ServiceIdlingResource::class.simpleName!!
    }
    override fun isIdleNow(): Boolean {
    val isIdle = !isIntentServiceRunning()
    if (isIdle) {
    mResourceCallback.onTransitionToIdle()
    }
    return isIdle
    }
    override fun registerIdleTransitionCallback(callback: ResourceCallback) {
    mResourceCallback = callback
    }
    fun isIntentServiceRunning() : Boolean { ... }
    }
    http://blog.sqisland.com/2015/04/espresso-custom-idling-resource.html

    View Slide

  46. Using IdlingResource
    @Before
    fun setUp() {
    mLoadingIdlingResource = ServiceIdlingResource()
    IdlingRegistry.getInstance().register(mLoadingIdlingResource)
    }
    @Test
    fun shouldVerifyMessageDuringDownloadingMoviesInfo() {
    onView(withId(R.id.fab))
    .perform(click())
    onView(withId(android.support.design.R.id.snackbar_text))
    .check(matches(withText(R.string.downloading_movie_info)))
    }
    @After
    fun tearDown() {
    IdlingRegistry.getInstance().unregister(mLoadingIdlingResource)
    }

    View Slide

  47. CountingIdlingResource
    class MovieDetailActivity : AppCompatActivity() {
    val mCountingIdlingResource = CountingIdlingResource(LOADING_MOVIE_DETAILS)
    companion object {
    private const val LOADING_MOVIE_DETAILS = "loading_movie_details"
    }
    fun loadMore() {
    ...
    mCountingIdlingResource.increment()
    }
    fun showMovies() {
    ...
    mCountingIdlingResource.decrement()
    }
    }

    View Slide

  48. Using CountingIdlingResource
    @Before
    fun setUp() {
    mLoadingIdlingResource = mMovieDetailActivityRule.
    activity.mCountingIdlingResource
    IdlingRegistry.getInstance().register(mLoadingIdlingResource)
    }
    @Test
    fun shouldVerifyMessageDuringDownloadingMoviesInfo() {
    onView(withId(R.id.fab))
    .perform(click())
    onView(withId(android.support.design.R.id.snackbar_text))
    .check(matches(withText(R.string.downloading_movie_info)))
    }
    @After
    fun tearDown() {
    IdlingRegistry.getInstance().unregister(mLoadingIdlingResource)
    }

    View Slide

  49. Configure a custom intent
    @Rule @JvmField
    val mMovieDetailActivityRule = ActivityTestRule(
    MovieDetailActivity::class.java,
    true,
    false) // is activity launch?
    @Test
    fun `Should display message during downloading movie info`() {
    val context = InstrumentationRegistry.getTargetContext()
    val movieIntent = Intent(context, MovieDetailActivity::class.java)
    movieIntent.putExtra("movie_id", ANNABELLE_MOVIE)
    mMovieDetailActivityRule.launchActivity(movieIntent)
    ...
    }
    App data: https://www.themoviedb.org

    View Slide

  50. Handle animation inside the app
    val animatorSpeed = Settings.Global.getFloat(​
    contentResolver,​
    SETTINGS_NAME,​
    0.0f)​

    ü Settings.Global.ANIMATOR_DURATION_SCALE ​
    ü Settings.Global.TRANSITION_ANIMATION_SCALE​
    ü Settings.Global.WINDOW_ANIMATION_SCALE​

    View Slide

  51. UI AutomatorViewer
    Android Studio
    Tools / Android / Android Device Monitor
    Application
    {ANDROID_SDK} / tools / uiautomatorviewer

    View Slide

  52. UiAutomator
    Android 4.3 (API 18)
    Interacting with any application
    Interacting with a system

    View Slide

  53. Basic UI Automator components
    ü UiDevice
    ü UiCollection
    ü UiObject / UiObject2
    ü UiScrollable
    ü UiSelector
    ü Configurator

    View Slide

  54. UiDevice
    ü Press "Home" button
    mUiDevice.pressHome()
    ü Press "Back" button
    mUiDevice.pressBack()
    ü Press “Power” button
    mUiDevice.wakeUp()
    ü Click at arbitrary coordinates
    mUiDevice.click(300, 215)
    ü Open the notifications
    mUiDevice.openNotification()

    View Slide

  55. Test of calculator application
    class CalculatorAppTest {
    lateinit var mDevice: UiDevice
    @Before fun setUp() {
    mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    mDevice.pressHome()
    mDevice.findObject(By.desc("Apps list")).click()
    mDevice.findObject(By.desc("Calculator")).click()
    }
    @Test
    fun shouldSumThreeAndFive() {
    mDevice.wait(Until.hasObject(By.text("5")), 3_000L)
    val five = mDevice.findObject(By.text("5"))
    val plus = mDevice.findObject(By.text("+"))
    val three = mDevice.findObject(By.text("3"))
    val equal = mDevice.findObject(By.text("="))
    five.click()
    plus.click()
    three.click()
    equal.click()
    val result = mDevice.findObject(By.res("com.android.calculator2", "result"))
    assertEquals("8", result.text)
    }
    @After fun tearDown() {
    mDevice.pressBack()
    }
    }

    View Slide

  56. Test of calculator application
    class CalculatorAppTest {
    lateinit var mDevice: UiDevice
    @Before fun setUp() {
    mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    mDevice.pressHome()
    mDevice.findObject(By.desc("Apps list")).click()
    mDevice.findObject(By.desc("Calculator")).click()
    }
    @Test
    fun shouldSumThreeAndFive() {
    mDevice.wait(Until.hasObject(By.text("5")), 3_000L)
    val five = mDevice.findObject(By.text("5"))
    val plus = mDevice.findObject(By.text("+"))
    val three = mDevice.findObject(By.text("3"))
    val equal = mDevice.findObject(By.text("="))
    five.click()
    plus.click()
    three.click()
    equal.click()
    val result = mDevice.findObject(By.res("com.android.calculator2", "result"))
    assertEquals("8", result.text)
    }
    @After fun tearDown() {
    mDevice.pressBack()
    }
    }

    View Slide

  57. Test of calculator application
    class CalculatorAppTest {
    lateinit var mDevice: UiDevice
    @Before fun setUp() {
    mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    mDevice.pressHome()
    mDevice.findObject(By.desc("Apps list")).click()
    mDevice.findObject(By.desc("Calculator")).click()
    }
    @Test
    fun shouldSumThreeAndFive() {
    mDevice.wait(Until.hasObject(By.text("5")), 3_000L)
    val five = mDevice.findObject(By.text("5"))
    val plus = mDevice.findObject(By.text("+"))
    val three = mDevice.findObject(By.text("3"))
    val equal = mDevice.findObject(By.text("="))
    five.click()
    plus.click()
    three.click()
    equal.click()
    val result = mDevice.findObject(By.res("com.android.calculator2", "result"))
    assertEquals("8", result.text)
    }
    @After fun tearDown() {
    mDevice.pressBack()
    }
    }

    View Slide

  58. Test of calculator application
    class CalculatorAppTest {
    lateinit var mDevice: UiDevice
    @Before
    fun setUp() {
    mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    mDevice.pressHome()
    mDevice.findObject(By.desc("Apps list")).click()
    mDevice.findObject(By.desc("Calculator")).click()
    }
    @Test
    fun shouldSumThreeAndFive() {
    mDevice.wait(Until.hasObject(By.text("5")), 3_000L)
    val five = mDevice.findObject(By.text("5"))
    val plus = mDevice.findObject(By.text("+"))
    val three = mDevice.findObject(By.text("3"))
    val equal = mDevice.findObject(By.text("="))
    five.click()
    plus.click()
    three.click()
    equal.click()
    val result = mDevice.findObject(By.res("com.android.calculator2", "result"))
    assertEquals("8", result.text)
    }
    @After
    fun tearDown() {
    mDevice.pressBack()
    }
    }

    View Slide

  59. Test of calculator application
    class CalculatorAppTest {
    lateinit var mDevice: UiDevice
    @Before
    fun setUp() {
    mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    mDevice.pressHome()
    mDevice.findObject(By.desc("Apps list")).click()
    mDevice.findObject(By.desc("Calculator")).click()
    }
    @Test
    fun shouldSumThreeAndFive() {
    mDevice.wait(Until.hasObject(By.text("5")), 3_000L)
    val five = mDevice.findObject(By.text("5"))
    val plus = mDevice.findObject(By.text("+"))
    val three = mDevice.findObject(By.text("3"))
    val equal = mDevice.findObject(By.text("="))
    five.click()
    plus.click()
    three.click()
    equal.click()
    val result = mDevice.findObject(By.res("com.android.calculator2", "result"))
    assertEquals("8", result.text)
    }
    @After
    fun tearDown() {
    mDevice.pressBack()
    }
    }

    View Slide

  60. Test of calculator application
    class CalculatorAppTest {
    lateinit var mDevice: UiDevice
    @Before
    fun setUp() {
    mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    mDevice.pressHome()
    mDevice.findObject(By.desc("Apps list")).click()
    mDevice.findObject(By.desc("Calculator")).click()
    }
    @Test
    fun shouldSumThreeAndFive() {
    mDevice.wait(Until.hasObject(By.text("5")), 3_000L)
    val five = mDevice.findObject(By.text("5"))
    val plus = mDevice.findObject(By.text("+"))
    val three = mDevice.findObject(By.text("3"))
    val equal = mDevice.findObject(By.text("="))
    five.click()
    plus.click()
    three.click()
    equal.click()
    val result = mDevice.findObject(By.res("com.android.calculator2", "result"))
    assertEquals("8", result.text)
    }
    @After
    fun tearDown() {
    mDevice.pressBack()
    }
    }

    View Slide

  61. Test of calculator application
    class CalculatorAppTest {
    lateinit var mDevice: UiDevice
    @Before
    fun setUp() {
    mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    mDevice.pressHome()
    mDevice.findObject(By.desc("Apps list")).click()
    mDevice.findObject(By.desc("Calculator")).click()
    }
    @Test
    fun shouldSumThreeAndFive() {
    mDevice.wait(Until.hasObject(By.text("5")), 3_000L)
    val five = mDevice.findObject(By.text("5"))
    val plus = mDevice.findObject(By.text("+"))
    val three = mDevice.findObject(By.text("3"))
    val equal = mDevice.findObject(By.text("="))
    five.click()
    plus.click()
    three.click()
    equal.click()
    val result = mDevice.findObject(By.res("com.android.calculator2", "result"))
    assertEquals("8", result.text)
    }
    @After
    fun tearDown() {
    mDevice.pressBack()
    }
    }

    View Slide

  62. Test of calculator application
    class CalculatorAppTest {
    lateinit var mDevice: UiDevice
    @Before
    fun setUp() {
    mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    mDevice.pressHome()
    mDevice.findObject(By.desc("Apps list")).click()
    mDevice.findObject(By.desc("Calculator")).click()
    }
    @Test
    fun shouldSumThreeAndFive() {
    mDevice.wait(Until.hasObject(By.text("5")), 3_000L)
    val five = mDevice.findObject(By.text("5"))
    val plus = mDevice.findObject(By.text("+"))
    val three = mDevice.findObject(By.text("3"))
    val equal = mDevice.findObject(By.text("="))
    five.click()
    plus.click()
    three.click()
    equal.click()
    val result = mDevice.findObject(By.res("com.android.calculator2", "result"))
    assertEquals("8", result.text)
    }
    @After
    fun tearDown() {
    mDevice.pressBack()
    }
    }

    View Slide

  63. Espresso + UiAutomator
    Interacting with a system
    Better interaction with the app

    View Slide

  64. @RunWith(AndroidJUnit4::class.java)
    class FullOrderTest {
    ...
    @Test
    fun shouldOrderThreeEspressos() {
    ...
    onView(withId(R.id.pay)).perform(click())
    mDevice.openNotification()
    mDevice.wait(Until.hasObject(
    By.text(NOTIFICATION_TITLE)), TIMEOUT)
    val title = mDevice.findObject(
    By.text(NOTIFICATION_TITLE))
    assertEquals(NOTIFICATION_TITLE, title.getText())
    ...
    }
    }

    View Slide

  65. @RunWith(AndroidJUnit4::class.java)
    class FullOrderTest {
    ...
    @Test
    fun shouldOrderThreeEspressos() {
    ...
    onView(withId(R.id.pay)).perform(click())
    mDevice.openNotification()
    mDevice.wait(Until.hasObject(
    By.text(NOTIFICATION_TITLE)), TIMEOUT)
    val title = mDevice.findObject(
    By.text(NOTIFICATION_TITLE))
    assertEquals(NOTIFICATION_TITLE, title.getText())
    ...
    }
    }

    View Slide

  66. Q & A 1. Testing Support library
    https://developer.android.com/topic/libraries/testing-support-library/
    2. Android Testing Codelab
    https://codelabs.developers.google.com/codelabs/android-testing/
    3. Espresso
    https://developer.android.com/training/testing/espresso/
    4. UiAutomator
    https://developer.android.com/training/testing/ui-automator.html
    5. Blog
    http://alexzh.com/

    View Slide