Slide 1

Slide 1 text

.

Slide 2

Slide 2 text

Welcome to Android World!

Slide 3

Slide 3 text

Application is growing

Slide 4

Slide 4 text

Application is growing

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

How to test the project?

Slide 7

Slide 7 text

Unit tests JUnit Hamcrest Mockito Robolectric

Slide 8

Slide 8 text

Integration tests JUnit Hamcrest Mockito Instrumentation API

Slide 9

Slide 9 text

Acceptance tests Espresso UiAutomator Robotium Calabash Appium

Slide 10

Slide 10 text

Android Testing Support Library AndroidJUnitRunner JUnit4 Rules Espresso UiAutomator

Slide 11

Slide 11 text

adb shell Instrumentation am instrument Application Test Application

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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)

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Espresso Android 2.2 (API 8) Turn off the animations

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Hamcrest matchers

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

@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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

onView(withId(R.id.download)) .perform(click()) onView(withId(android.support.design.R.id.snackbar_text)) .check(matches( withText(R.string.downloading_movie_info)))

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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)

Slide 35

Slide 35 text

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] } }

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

ü Message Queue ü AsyncTask ü Idling Resources Espresso synchronization

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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​

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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