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

    .

  2. 5.

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

    Require Android device (instrumentation tests) Tests
  3. 13.
  4. 14.

    GrantPermissionRule ü grant(varargs permissions : String!) @Rule @JvmField val permissionRule

    = GrantPermissionRule.grant( android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.WRITE_EXTERNAL_STORAGE )
  5. 15.

    Test rule order class MultipleRulesTest { @Rule @JvmField val firstLog

    = LogRule("First") @Rule @JvmField val secondLog = LogRule("Second") @Test fun test() { assertEquals(1, 1) } }
  6. 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
  7. 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
  8. 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
  9. 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") } } } }
  10. 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")))) } }
  11. 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")))) } }
  12. 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
  13. 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()) } } }
  14. 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
  15. 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] } }
  16. 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 } } }
  17. 38.

    RecyclerViewActions ü actionOnHolderItem ü actionOnItem ü actionOnItemAtPosition ü scrollTo ü

    scrollToHolder ü scrollToPosition onView(withId(R.id.recyclerView)) .perform(actionOnItemAtPosition(3, click())) App data: https://www.themoviedb.org
  18. 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
  19. 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) }
  20. 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() } }
  21. 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) }
  22. 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
  23. 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​
  24. 46.

    UI AutomatorViewer Android Studio Tools / Android / Android Device

    Monitor Application {ANDROID_SDK} / tools / uiautomatorviewer
  25. 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() } }
  26. 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() } }
  27. 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() } }
  28. 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() } }
  29. 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() } }
  30. 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() } }
  31. 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() } }
  32. 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() } }
  33. 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()) ... } }
  34. 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()) ... } }
  35. 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