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.

2b0404a5db1a74f01bf3bf94d142e28c?s=128

Alex Zhukovich

October 21, 2017
Tweet

Transcript

  1. .

  2. Welcome to Android World!

  3. Application is growing

  4. Application is growing

  5. Categories of Android tests Run on the JVM (local tests)

    Require Android device (instrumentation tests) Tests
  6. How to test the project?

  7. Unit tests JUnit Hamcrest Mockito Robolectric

  8. Integration tests JUnit Hamcrest Mockito Instrumentation API

  9. Acceptance tests Espresso UiAutomator Robotium Calabash Appium

  10. Android Testing Support Library AndroidJUnitRunner JUnit4 Rules Espresso UiAutomator

  11. adb shell Instrumentation am instrument Application Test Application

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

    ü Report results
  13. JUnit4 Rules ü ActivityTestRule ü IntentsTestRule ü ServiceTestRule ü ProviderTestRule

    ü GrantPermissionRule (API 23 – Android Marshmallow)
  14. GrantPermissionRule ü grant(varargs permissions : String!) @Rule @JvmField val permissionRule

    = GrantPermissionRule.grant( android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.WRITE_EXTERNAL_STORAGE )
  15. Test rule order class MultipleRulesTest { @Rule @JvmField val firstLog

    = LogRule("First") @Rule @JvmField val secondLog = LogRule("Second") @Test fun test() { assertEquals(1, 1) } }
  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
  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
  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
  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)
  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") } } } }
  21. Espresso Android 2.2 (API 8) Turn off the animations

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

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

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

  25. Hamcrest matchers

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

  27. @RunWith(AndroidJUnit4::class) class LoginActivityTest { @Rule @JvmField val mLoginActivityRule = ActivityTestRule<LoginActivity>(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")))) } }
  28. @RunWith(AndroidJUnit4::class) class LoginActivityTest { @Rule @JvmField val mLoginActivityRule = ActivityTestRule<LoginActivity>(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")))) } }
  29. @RunWith(AndroidJUnit4::class) class LoginActivityTest { @Rule @JvmField val mLoginActivityRule = ActivityTestRule<LoginActivity>(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
  30. fun withError(expected: Matcher<String>): Matcher<View> { return object : TypeSafeMatcher<View>() {

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

  32. What is an AdapterView and how it works? java.lang.Object ↳

    android.view.View ↳ android.view.ViewGroup ↳ android.widget.AdapterView <T extends android.widget.Adapter> GridView ListView Spinner
  33. Espresso onData(ObjectMatcher) .DataOptions .perform(ViewAction) .check(ViewAssertion)

  34. onData – ArrayAdapter onData(allOf(`is`(instanceOf(String::class.java)), `is`("Americano"))) .check(matches(isDisplayed())) val arrayAdapter = ArrayAdapter<String>(

    this@DetailActivity, android.R.layout.simple_list_item_1, android.R.id.text1, coffeeList)
  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] } }
  36. fun withCoffee(expected: Coffee) : Matcher<Any> { return object : BoundedMatcher<Any,

    Coffee>(Coffee::class.java) { override fun describeTo(description: Description) { description.appendText("has value: $expected") } override fun matchesSafely(item: Coffee): Boolean { return item == expected } } }
  37. RecyclerView java.lang.Object ↳ android.view.View ↳ android.view.ViewGroup ↳ android.support.v7.widget.RecyclerView It is

    not an AdapterView
  38. RecyclerViewActions ü actionOnHolderItem ü actionOnItem ü actionOnItemAtPosition ü scrollTo ü

    scrollToHolder ü scrollToPosition onView(withId(R.id.recyclerView)) .perform(actionOnItemAtPosition(3, click())) App data: https://www.themoviedb.org
  39. ü Message Queue ü AsyncTask ü Idling Resources Espresso synchronization

  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
  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) }
  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() } }
  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) }
  44. Configure a custom intent @Rule @JvmField val mMovieDetailActivityRule = ActivityTestRule<MovieDetailActivity>(

    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
  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​
  46. UI AutomatorViewer Android Studio Tools / Android / Android Device

    Monitor Application {ANDROID_SDK} / tools / uiautomatorviewer
  47. UiAutomator Android 4.3 (API 18) Interacting with any applications Interacting

    with a system
  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() } }
  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() } }
  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() } }
  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() } }
  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() } }
  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() } }
  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() } }
  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() } }
  56. Espresso + UiAutomator Interacting with a system Better interaction with

    the app
  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()) ... } }
  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()) ... } }
  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