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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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)
    }
    }

    View 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)
    }
    }
    Second - Start
    First - Start
    Running the test
    First - End
    Second - End

    View Slide

  17. 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 Slide

  18. 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 Slide

  19. 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 Slide

  20. 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  25. Hamcrest matchers

    View Slide

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

    View 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 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"))))
    }
    }

    View Slide

  29. @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 Slide

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

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

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

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

    View Slide

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

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

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

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

    View Slide

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

    View Slide

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

    View Slide

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

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

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

  43. 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 Slide

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

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

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

    View Slide

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

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

    View 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 Slide

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

  59. 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 Slide