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

Drink Espresso During Android Testing - Lodz, Mobilization

Drink Espresso During Android Testing - Lodz, Mobilization

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

October 21, 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. adb shell
    Instrumentation
    am instrument
    Application
    Test Application

    View full-size slide

  11. AndroidJUnitRunner
    ü JUnit4 support
    ü Collect tests
    ü Execute tests
    ü Report results

    View full-size slide

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

    View full-size slide

  13. GrantPermissionRule
    ü grant(varargs permissions : String!)
    @Rule @JvmField
    val permissionRule = GrantPermissionRule.grant(
    android.Manifest.permission.ACCESS_FINE_LOCATION,
    android.Manifest.permission.WRITE_EXTERNAL_STORAGE
    )

    View full-size slide

  14. Test rule order
    class MultipleRulesTest {
    @Rule @JvmField
    val firstLog = LogRule("First")
    @Rule @JvmField
    val secondLog = LogRule("Second")
    @Test
    fun test() {
    assertEquals(1, 1)
    }
    }

    View full-size slide

  15. Test rule order
    class MultipleRulesTest {
    @Rule @JvmField
    val firstLog = LogRule("First")
    @Rule @JvmField
    val secondLog = LogRule("Second")
    @Test
    fun test() {
    assertEquals(1, 1)
    }
    }
    Second - Start
    First - Start
    Running the test
    First - End
    Second - End

    View full-size slide

  16. Test rule order
    class MultipleRulesTest {
    @Rule @JvmField
    val firstLog = LogRule("First")
    @Rule @JvmField
    val secondLog = LogRule("Second")
    @Test
    fun test() {
    assertEquals(1, 1)
    }
    }
    JVM
    Second - Start
    First - Start
    Running the test
    First - End
    Second - End

    View full-size slide

  17. Control the rule order
    class MultipleRulesTest {
    @Rule @JvmField
    val mChain = RuleChain.outerRule(LogRule("First"))
    .around(LogRule("Second"))
    @Test
    fun test() {
    assertEquals(1, 1)
    }
    }
    First - Start
    Second - Start
    Running the test
    Second - End
    First - End

    View full-size slide

  18. val mTempFolderRule = TemporaryFolder(getContext().cacheDir)
    val mPermissionRule = GrantPermissionRule.grant(
    Manifest.permission.WRITE_EXTERNAL_STORAGE,
    Manifest.permission.READ_EXTERNAL_STORAGE)
    @Rule @JvmField
    val mChain = RuleChain.outerRule(mPermissionRule)
    .around(mTempFolderRule)

    View full-size slide

  19. Custom rule
    class LogRule(text: String): TestRule {
    val logText = text
    override fun apply(base: Statement, description: Description): Statement {
    return object : Statement() {
    override fun evaluate() {
    println("$logText - Start")
    base.evaluate()
    println("$logText - End")
    }
    }
    }
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  22. onView(withId(R.id.name))
    .perform(typeText("Alex"))
    onView(withId(R.id.pay))
    .check(matches(isDisplayed()))
    .perform(click())

    View full-size slide

  23. onView(withId(R.id.name))
    .perform(typeText("Alex"))
    onView(withId(R.id.pay))
    .check(matches(isDisplayed()))
    .perform(click())

    View full-size slide

  24. Hamcrest matchers

    View full-size slide

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

    View full-size slide

  26. @RunWith(AndroidJUnit4::class)
    class LoginActivityTest {
    @Rule @JvmField
    val mLoginActivityRule = ActivityTestRule(LoginActivity::class.java)
    @Test
    fun shouldVerifyEmptyPassword() {
    onView(withId(R.id.edit_text_email))
    .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

  27. @RunWith(AndroidJUnit4::class)
    class LoginActivityTest {
    @Rule @JvmField
    val mLoginActivityRule = ActivityTestRule(LoginActivity::class.java)
    @Test
    fun shouldVerifyEmptyPassword() {
    onView(withId(R.id.edit_text_email))
    .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

  28. @RunWith(AndroidJUnit4::class)
    class LoginActivityTest {
    @Rule @JvmField
    val mLoginActivityRule = ActivityTestRule(LoginActivity::class.java)
    @Test
    fun shouldVerifyEmptyPassword() {
    onView(withId(R.id.edit_text_email))
    .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

  29. fun withError(expected: Matcher): Matcher {
    return object : TypeSafeMatcher() {
    override fun matchesSafely(item: View): Boolean {
    return if (item is TextInputLayout) {
    expected.matches(item.error.toString())
    } else false
    }
    override fun describeTo(description: Description) {
    description.appendText("with error message: ")
    description.appendText(expected.toString())
    }
    }
    }

    View full-size slide

  30. 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

  31. 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

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

    View full-size slide

  33. 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

  34. 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

  35. 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

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

    View full-size slide

  37. RecyclerViewActions
    ü actionOnHolderItem
    ü actionOnItem
    ü actionOnItemAtPosition
    ü scrollTo
    ü scrollToHolder
    ü scrollToPosition
    onView(withId(R.id.recyclerView))
    .perform(actionOnItemAtPosition(3, click()))
    App data: https://www.themoviedb.org

    View full-size slide

  38. ü Message Queue
    ü AsyncTask
    ü Idling Resources
    Espresso synchronization

    View full-size slide

  39. 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

  40. Using IdlingResource
    @Before
    fun setUp() {
    mLoadingIdlingResource = ServiceIdlingResource()
    IdlingRegistry.getInstance().register(mLoadingIdlingResource)
    }
    @Test
    fun shouldVerifyMessageDuringDownloading () {
    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

  41. 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

  42. Using CountingIdlingResource
    @Before
    fun setUp() {
    var mLoadingIdlingResource = mMovieDetailActivityRule
    .activity.mCountingIdlingResource
    IdlingRegistry.getInstance().register(mLoadingIdlingResource)
    }
    @Test
    fun shouldVerifyMessageDuringDownloading () {
    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

  43. Configure a custom intent
    @Rule @JvmField
    val mMovieDetailActivityRule = ActivityTestRule(
    MovieDetailActivity::class.java,
    true,
    false) // is activity launch?
    @Test
    fun shouldDisplayMessageDuringDownloading() {
    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

  44. 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

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

    View full-size slide

  46. UiAutomator
    Android 4.3 (API 18)
    Interacting with any applications
    Interacting with a system

    View full-size slide

  47. 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

  48. 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

  49. 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

  50. 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

  51. 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

  52. 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

  53. 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

  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. Espresso + UiAutomator
    Interacting with a system
    Better interaction with the app

    View full-size slide

  56. @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

  57. @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

  58. 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/
    @Alex_Zhukovich

    View full-size slide