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

Android testing with Kotlin

Android testing with Kotlin

Avatar for markiyan-antonyuk

markiyan-antonyuk

December 26, 2019
Tweet

More Decks by markiyan-antonyuk

Other Decks in Programming

Transcript

  1. What Software testing is an investigation conducted to provide stakeholders

    with information about the quality of the software product or service under test. Software testing can also provide an objective, independent view of the software to allow the business to appreciate and understand the risks of software implementation. Test techniques include the process of executing a program or application with the intent of finding software bugs (errors or other defects), and verifying that the software product is fit for use.
  2. What Software testing is an investigation conducted to provide stakeholders

    with information about the quality of the software product or service under test. Software testing can also provide an objective, independent view of the software to allow the business to appreciate and understand the risks of software implementation. Test techniques include the process of executing a program or application with the intent of finding software bugs (errors or other defects), and verifying that the software product is fit for use. Process to check software quality and to push us to work more.
  3. Why

  4. Unit testing Unit tests are the fundamental tests in your

    app testing strategy. By creating and running unit tests against your code, you can easily verify that the logic of individual unit is correct. Running unit tests after every build helps you to quickly catch and fix software regressions introduced by code changes to your app.
  5. Unit testing Unit tests are the fundamental tests in your

    app testing strategy. By creating and running unit tests against your code, you can easily verify that the logic of individual units is correct. Running unit tests after every build helps you to quickly catch and fix software regressions introduced by code changes to your app.
  6. class UnitTest { <- test class fun `test 2 plus

    2 is 4`() { <- test function assertEquals(4, 2 + 2) <- assertion } }
  7. class UnitTest { <- test class @Test <- @Test annotation

    to mark function as test fun `test 2 plus 2 is 4`() { <- test function assertEquals(4, 2 + 2) <- assertion } }
  8. class UnitTest { <- test class @Test <- @Test annotation

    to mark function as test fun `test 2 plus 2 is 4`() { <- test function assertEquals(4, 2 + 2) <- assertion } } @Suite.SuiteClasses(UnitTest::class) <- suite
  9. AAA - Arrange, Act, Assert @Test fun `test 2 plus

    4 is 4`() { // arrange (initialize) val first = 2 val second = 2 // act (do job) val result = first + second // assert (verify) assertEquals(4, result) }
  10. JUnit @Before <- before each test function @After <- after

    each test function @BeforeClass <- before execution of test class (static context) @AfterClass <- after execution of test class (static context)
  11. Android Unit testing Unit tests that run on your local

    machine only. These tests are compiled to run locally on the Java Virtual Machine (JVM) to minimize execution time. If your tests depend on objects in the Android framework, we recommend using Robolectric. For tests that depend on your own dependencies, use mock objects to emulate your dependencies' behavior.
  12. Android Unit testing Unit tests that run on your local

    machine only. These tests are compiled to run locally on the Java Virtual Machine (JVM) to minimize execution time. If your tests depend on objects in the Android framework, we recommend using Robolectric. For tests that depend on your own dependencies, use mock objects to emulate your dependencies' behavior.
  13. @Test fun `test login should return user from data source`()

    { val user = User(0, "KLUG") val loginRepository = LoginRepository(dataSource) }
  14. @Test fun `test login should return user from data source`()

    { val user = User(0, "KLUG") val dataSource = object : DataSource { override fun login(login: String, password: String): User = user } val loginRepository = LoginRepository(dataSource) }
  15. @Test fun `test login should return user from data source`()

    { val user = User(0, "KLUG") val dataSource = object : DataSource { override fun login(login: String, password: String): User = user } val loginRepository = LoginRepository(dataSource) val userResult = loginRepository.login("User", "Password") }
  16. @Test fun `test login should return user from data source`()

    { val user = User(0, "KLUG") val dataSource = object : DataSource { override fun login(login: String, password: String): User = user } val loginRepository = LoginRepository(dataSource) val userResult = loginRepository.login("User", "Password") assertEquals(user, userResult) }
  17. @Test fun `test login should return user from data source`()

    { val user = User(0, "KLUG") var called = false val dataSource = object : DataSource { override fun login(login: String, password: String): User { called = true return user } } val loginRepository = LoginRepository(dataSource) val userResult = loginRepository.login("User", "Password") assertEquals(user, userResult) assertTrue(called) }
  18. @Test fun `test login should return user from data source`()

    { val user = User(0, "KLUG") var called = false val dataSource = object : DataSource { override fun login(login: String, password: String): User { Is proper login/pass passed? called = true return user } } val loginRepository = LoginRepository(dataSource) val userResult = loginRepository.login("User", "Password") assertEquals(user, userResult) assertTrue(called) }
  19. @Test fun `test login should return user from data source`()

    { val user = User(0, "KLUG") var called = false val dataSource = object : DataSource { What if DataSource is final class? override fun login(login: String, password: String): User { Is proper login/pass passed? called = true return user } } val loginRepository = LoginRepository(dataSource) val userResult = loginRepository.login("User", "Password") assertEquals(user, userResult) assertTrue(called) }
  20. MockK https://mockk.io/ • Mocking • Object, Constructor, Extensions mocking •

    DSL • Verification order • Coroutines • … and much more
  21. @Test fun `test login should return user from data source`()

    { val user = User(0, "KLUG") var called = false val dataSource = object : DataSource { What if DataSource is final class? override fun login(login: String, password: String): User { Is proper login/pass passed? called = true return user } } val loginRepository = LoginRepository(dataSource) val userResult = loginRepository.login("User", "Password") assertEquals(user, userResult) assertTrue(called) }
  22. @Test fun `test login should return user from data source`()

    { val user = User(0, "KLUG") val loginRepository = LoginRepository(dataSource) val userResult = loginRepository.login("User", "Password") assertEquals(user, userResult) }
  23. @Test fun `test login should return user from data source`()

    { val user = User(0, "KLUG") val dataSource = mockk<DataSource>() { every { login(any(), any()) } returns user } val loginRepository = LoginRepository(dataSource) val userResult = loginRepository.login("User", "Password") assertEquals(user, userResult) }
  24. inline fun <reified T : Any> mockk( name: String? =

    null, relaxed: Boolean = false, vararg moreInterfaces: KClass<*>, relaxUnitFun: Boolean = false, block: T.() -> Unit = {} )
  25. @Test fun `test login should return user from data source`()

    { val user = User(0, "KLUG") val dataSource = mockk<DataSource>() { every { login(any(), any()) } returns user } val loginRepository = LoginRepository(dataSource) val userResult = loginRepository.login("User", "Password") assertEquals(user, userResult) }
  26. @Test fun `test login should return user from data source`()

    { val user = User(0, "KLUG") val dataSource = mockk<DataSource>() { every { login(any(), any()) } returns user } val loginRepository = LoginRepository(dataSource) val userResult = loginRepository.login("User", "Password") verify { dataSource.login("User", "Password") } assertEquals(user, userResult) }
  27. fun verify( ordering: Ordering = Ordering.UNORDERED, inverse: Boolean = false,

    atLeast: Int = 1, atMost: Int = Int.MAX_VALUE, exactly: Int = -1, timeout: Long = 0, verifyBlock: MockKVerificationScope.() -> Unit )
  28. @Test fun `test login should return user from data source`()

    { val user = User(0, "KLUG") val dataSource = mockk<DataSource>() { every { login(any(), any()) } returns user } val loginRepository = LoginRepository(dataSource) val userResult = loginRepository.login("User", "Password") verify { dataSource.login("User", "Password") } assertEquals(user, userResult) }
  29. Object @Test fun `object mocking`() { assertEquals(1, Object.returnOne()) mockkObject(Object) every

    { Object.returnOne() } returns 2 assertEquals(2, Object.returnOne()) unmockkObject(Object) assertEquals(1, Object.returnOne()) }
  30. Extensions @Test fun `extensions mocking`() { mockkStatic("kotlin.io.FilesKt__UtilsKt") or class with

    extensions every { File("/").deleteRecursively() } returns true assertTrue(File("/").deleteRecursively()) unmockkStatic("kotlin.io.FilesKt__UtilsKt") }
  31. Extensions @Test fun `extensions mocking`() { mockkStatic("kotlin.io.FilesKt__UtilsKt") or class with

    extensions every { File("/").deleteRecursively() } returns true assertTrue(File("/").deleteRecursively()) unmockkStatic("kotlin.io.FilesKt__UtilsKt") } every { File("/").deleteRecursively() } answers { throw Exception("") }
  32. Switch implementation @Test fun `switch implementation`() { val scheme =

    Uri.parse("http://myuri").scheme assertEquals("http", scheme) }
  33. Switch implementation @Test fun `switch implementation`() { val scheme =

    Uri.parse("http://myuri").scheme assertEquals("http", scheme) } java.lang.RuntimeException: Method parse in android.net.Uri not mocked.
  34. package android.net; public abstract class Uri implements Comparable<Uri>, Parcelable {

    public static Uri parse(String uriString) { return new Uri() { … } public String getScheme() { return "http"; } }
  35. LiveData & ViewModel class LoginViewModel( private val repository: Repository )

    : ViewModel() { private val _userLiveData = MutableLiveData<User>() val userLiveData: LiveData<User> = _userLiveData fun login(email: String, pass: String) { viewModelScope.launch(Dispatchers.IO) { val user = repository.login(email, pass) _userLiveData.postValue(user) } …
  36. @Test fun `test login`() { val repository = mockk<Repository>() val

    observer = mockk<Observer<User>>() val viewmodel = LoginViewModel(repository) viewmodel.userLiveData.observeForever(observer) viewmodel.login("email", "pass") verify { observer.onChanged(any()) } }
  37. @Test fun `test login`() { val repository = mockk<Repository>() val

    observer = mockk<Observer<User>>() val viewmodel = LoginViewModel(repository) viewmodel.userLiveData.observeForever(observer) viewmodel.login("email", "pass") _userLiveData.postValue(user) verify { observer.onChanged(any()) } }
  38. @get:Rule val rule = InstantTaskExecutorRule() @Test fun `test login`() {

    val repository = mockk<Repository>() val observer = mockk<Observer<User>>() val viewmodel = LoginViewModel(repository) viewmodel.userLiveData.observeForever(observer) viewmodel.login("email", "pass") verify { observer.onChanged(any()) } }
  39. @get:Rule val rule = InstantTaskExecutorRule() @Test viewModelScope.launch(Dispatchers.IO) fun `test login`()

    { val repository = mockk<Repository>() val observer = mockk<Observer<User>>() val viewmodel = LoginViewModel(repository) viewmodel.userLiveData.observeForever(observer) viewmodel.login("email", "pass") verify { observer.onChanged(any()) } }
  40. class LoginViewModel( private val repository: Repository, private val dispatcher: CoroutineDispatcher

    ) : ViewModel() … @Test fun `test login`() { … val viewmodel = LoginViewModel(repository, Dispatchers.Unconfined) … }
  41. • Data should be static • Different sets of data

    • Server errors, can’t emulate all
  42. • Data should be static • Different sets of data

    • Server errors, can’t emulate all • Authenticate each test run
  43. • Data should be static • Different sets of data

    • Server errors, can’t emulate all • Authenticate each test run • Response duration
  44. • Data should be static • Different sets of data

    • Server errors, can’t emulate all • Authenticate each test run • Response duration • Can be down
  45. • Data should be static • Different sets of data

    • Server errors, can’t emulate all • Authenticate each test run • Response duration • Can be down • This is AQA work
  46. • You decide whether to do error or what data

    to return • Tests and app components are independent of each other
  47. • You decide whether to do error or what data

    to return • Tests and app components are independent of each other • Do not rely on anything except app
  48. • You decide whether to do error or what data

    to return • Tests and app components are independent of each other • Do not rely on anything except app • You’re the BOSS here
  49. Mock ViewModel val userLiveData = MutableLiveData<User>() val viewModel = mockk<LoginViewModel>()

    { every { user } returns userLiveData } injectViewModel(viewModel) rule.launchActivity(null) userLiveData.postValue(User(0, "Markiyan"))
  50. Mock ViewModel val userLiveData = MutableLiveData<User>() val viewModel = mockk<LoginViewModel>()

    { every { user } returns userLiveData } injectViewModel(viewModel) rule.launchActivity(null) userLiveData.postValue(User(0, "Markiyan"))
  51. class LoginViewModel( private val repository: Repository, private val dispatcher: CoroutineDispatcher

    ) : ViewModel() { val user = MutableLiveData<User>() val loading = MutableLiveData<Boolean>() val error = MutableLiveData<Throwable>() …
  52. open class LoginViewModel( private val repository: Repository, private val dispatcher:

    CoroutineDispatcher ) : ViewModel() { open val user = MutableLiveData<User>() open val loading = MutableLiveData<Boolean>() open val error = MutableLiveData<Throwable>() …
  53. @OpenForTesting class LoginViewModel( private val repository: Repository, private val dispatcher:

    CoroutineDispatcher ) : ViewModel() { val user = MutableLiveData<User>() val loading = MutableLiveData<Boolean>() val error = MutableLiveData<Throwable>() …
  54. @OpenForTesting class LoginViewModel( private val repository: Repository, private val dispatcher:

    CoroutineDispatcher ) : ViewModel() { val user = MutableLiveData<User>() val loading = MutableLiveData<Boolean>() val error = MutableLiveData<Throwable>() …
  55. Use extensions private fun Int.asView() = onView(withId(this)) fun ViewInteraction.click() =

    perform(ViewActions.click()) fun Int.click() = asView().click()
  56. Use extensions private fun Int.asView() = onView(withId(this)) fun ViewInteraction.click() =

    perform(ViewActions.click()) fun Int.click() = asView().click() infix fun ViewInteraction.typeText(text: String) = perform(ViewActions.typeText(text)) infix fun Int.typeText(text: String) = asView().typeText(text)
  57. Use extensions private fun Int.asView() = onView(withId(this)) fun ViewInteraction.click() =

    perform(ViewActions.click()) fun Int.click() = asView().click() infix fun ViewInteraction.typeText(text: String) = perform(ViewActions.typeText(text)) infix fun Int.typeText(text: String) = asView().typeText(text) import <your.package.name>.R.id.*
  58. @Test fun testLogin() { etLogin typeText "login" etPassword typeText "password"

    btnLogin.click() imgUserAvatar.isDisplayed() tvUserName.isDisplayed() withText "User Name" }
  59. Espresso Packages • Core - Matchers, Actions • Intents -

    Intents - record, stub, verify • Web - WebView testing
  60. Espresso Packages • Core - Matchers, Actions • Intents -

    Intents - record, stub, verify • Web - WebView testing • Remote - Multi-process testing
  61. Espresso Packages • Core - Matchers, Actions • Intents -

    Intents - record, stub, verify • Web - WebView testing • Remote - Multi-process testing • Contrib - RecyclerView, Drawer, DatePicker
  62. Espresso Packages • Core - Matchers, Actions • Intents -

    Intents - record, stub, verify • Web - WebView testing • Remote - Multi-process testing • Contrib - RecyclerView, Drawer, DatePicker • Idling resource - Synchronize jobs
  63. UI loading something, custom animations IdlingResource Espresso's mechanism for synchronization

    with background jobs. • CountingIdlingResource • UriIdlingResource • IdlingThreadPoolExecutor / ScheduledIdlingThreadPoolExecutor
  64. Idling Resource implementation 'androidx.test.espresso:espresso-idling-resource:3.2.0' object SplashIdlingResource : IdlingResource { override

    fun isIdleNow(): Boolean = isIdle override fun registerIdleTransitionCallback(callback) { this.callback = callback } }
  65. Idling Resource implementation 'androidx.test.espresso:espresso-idling-resource:3.2.0' object SplashIdlingResource : IdlingResource { override

    fun isIdleNow(): Boolean = isIdle override fun registerIdleTransitionCallback(callback) { this.callback = callback } fun notifyJobStarted() fun notifyJobFinished() }
  66. Idling Resource implementation 'androidx.test.espresso:espresso-idling-resource:3.2.0' object SplashIdlingResource : IdlingResource { override

    fun isIdleNow(): Boolean = isIdle override fun registerIdleTransitionCallback(callback) { this.callback = callback } fun notifyJobStarted() { isIdle = false } fun notifyJobFinished() { isIdle = true callback?.onTransitionToIdle() }
  67. Idling Resource object SplashIdlingResource : IdlingResource { override fun isIdleNow():

    Boolean = isIdle override fun registerIdleTransitionCallback(callback) { this.callback = callback } fun notifyJobStarted() { isIdle = false } fun notifyJobFinished() { isIdle = true callback?.onTransitionToIdle() } IdlingRegistry.getInstance().register(SplashIdlingResource)
  68. Single-Activity inline fun <reified F : Fragment> launchFragmentInContainer( fragmentArgs: Bundle?

    = null, @StyleRes themeResId: Int = R.style.FragmentScenarioEmptyFragmentActivityTheme, factory: FragmentFactory? = null )
  69. Single-Activity inline fun <reified F : Fragment> launchFragmentInContainer( fragmentArgs: Bundle?

    = null, @StyleRes themeResId: Int = R.style.FragmentScenarioEmptyFragmentActivityTheme, factory: FragmentFactory? = null ) moveToState(Lifecycle.State)
  70. Single-Activity inline fun <reified F : Fragment> launchFragmentInContainer( fragmentArgs: Bundle?

    = null, @StyleRes themeResId: Int = R.style.FragmentScenarioEmptyFragmentActivityTheme, factory: FragmentFactory? = null ) moveToState(Lifecycle.State) recreate()
  71. Single-Activity inline fun <reified F : Fragment> launchFragmentInContainer( fragmentArgs: Bundle?

    = null, @StyleRes themeResId: Int = R.style.FragmentScenarioEmptyFragmentActivityTheme, factory: FragmentFactory? = null ) moveToState(Lifecycle.State) recreate() onFragment {}