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. Welcome to Android World!

    View full-size slide

  2. Application is growing

    View full-size slide

  3. Application is growing

    View full-size slide

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

    View full-size slide

  5. How to test the project?

    View full-size slide

  6. Unit tests
    JUnit
    Hamcrest
    Mockito
    Robolectric

    View full-size slide

  7. Integration tests
    JUnit
    Hamcrest
    Mockito
    Instrumentation
    API

    View full-size slide

  8. Acceptance tests
    Espresso
    UiAutomator
    Robotium
    Calabash
    Appium

    View full-size slide

  9. Android Testing Support Library
    AndroidJUnitRunner
    JUnit4 Rules
    Espresso
    UiAutomator

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  28. Hamcrest matchers

    View full-size slide

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

    View full-size slide

  30. @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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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"))))
    }
    }
    * withError – a custom matcher

    View full-size slide

  36. 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 full-size slide

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

    GridView ListView
    Spinner

    View full-size slide

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

    View full-size slide

  39. onData – ArrayAdapter
    onData(allOf(`is`(instanceOf(String::class.java)),
    `is`("Americano")))
    .check(matches(isDisplayed()))
    val arrayAdapter = ArrayAdapter(
    this@DetailActivity,
    android.R.layout.simple_list_item_1,
    android.R.id.text1,
    coffeeList)

    View full-size slide

  40. 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 full-size slide

  41. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  44. 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 full-size slide

  45. 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 full-size slide

  46. 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 full-size slide

  47. 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 full-size slide

  48. 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 full-size slide

  49. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  53. 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 full-size slide

  54. 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

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

    View full-size slide

  63. @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 full-size 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 full-size slide

  65. 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 full-size slide