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

Android Worldwide TDD in Android

Android Worldwide TDD in Android

Presented at Android Worldwide conference 2022.
Most developers (who writes test cases, but doesn't yet follow TDD) are interested in or willing to follow TDD, the only factor stopping them from following TDD is confusion.
In this session, I'll try to remove that confusion, so that developers can follow TDD without hesitation.
I'll be talking about following TDD, what should be the process, and how exactly should you approach TDD.

Rivu Chakraborty

April 19, 2022
Tweet

More Decks by Rivu Chakraborty

Other Decks in Technology

Transcript

  1. TDD in Android
    Rivu Chakraborty
    https://www.rivu.dev/
    AndroidWorldwide

    View full-size slide

  2. Rivu Chakraborty
    @rivuchakraborty
    https://www.rivu.dev
    Android @

    View full-size slide

  3. Types of Tests
    https://www.rivu.dev/
    https://www.rivu.dev/

    View full-size slide

  4. Test Pyramid
    Unit Tests
    Integration Tests
    E2E Tests

    View full-size slide

  5. Unit Tests
    @Test
    fun `test getUsers`() = runBlocking {
    val expected = getDummyUsers()
    val result = testUserRepository.getUsers()
    assertEquals(expected, result)
    }

    View full-size slide

  6. Integration Tests
    @Test
    fun `test getUsers`() = runBlocking {
    val expected = localDS.getUsers() //Actual Local DS
    val result = testUserRepository.getUsers().first()
    assertEquals(expected, result)
    }

    View full-size slide

  7. What is TDD????
    https://www.rivu.dev/
    https://www.rivu.dev/

    View full-size slide

  8. Test Driven Development
    ● Write Tests, let them fail and then write Implementation
    https://www.rivu.dev/

    View full-size slide

  9. Test Driven Development
    ● Write Tests, let them fail and then write Implementation
    ● We want Tests to fail first
    https://www.rivu.dev/

    View full-size slide

  10. Test Driven Development
    ● Write Tests, let them fail and then write Implementation
    ● We want Tests to fail first
    ● Steps
    ○ Write the Interface
    https://www.rivu.dev/

    View full-size slide

  11. Test Driven Development
    ● Write Tests, let them fail and then write Implementation
    ● We want Tests to fail first
    ● Steps
    ○ Write the Interface
    ○ Design the Flow
    https://www.rivu.dev/

    View full-size slide

  12. Test Driven Development
    ● Write Tests, let them fail and then write Implementation
    ● We want Tests to fail first
    ● Steps
    ○ Write the Interface
    ○ Design the Flow
    ○ Implement the methods with TODO (We still want them to compile 😉)
    https://www.rivu.dev/

    View full-size slide

  13. Test Driven Development
    ● Write Tests, let them fail and then write Implementation
    ● We want Tests to fail first
    ● Steps
    ○ Write the Interface
    ○ Design the Flow
    ○ Implement the methods with TODO (We still want them to compile 😉)
    ○ Write the Tests and let them fail
    https://www.rivu.dev/

    View full-size slide

  14. Test Driven Development
    ● Write Tests, let them fail and then write Implementation
    ● We want Tests to fail first
    ● Steps
    ○ Write the Interface
    ○ Design the Flow
    ○ Implement the methods with TODO (We still want them to compile 😉)
    ○ Write the Implementation, make sure the tests pass now
    https://www.rivu.dev/

    View full-size slide

  15. Let’s do it
    https://www.rivu.dev/
    https://www.rivu.dev/

    View full-size slide

  16. Write the Interface
    interface ArticlesRepository {
    fun getArticles(): Flowable>
    fun fetchAndSyncArticles(): Single>
    }
    https://www.rivu.dev/

    View full-size slide

  17. Design the Flow
    https://www.rivu.dev/
    ● getArticles() should return from Local first and then after syncing should
    return from Remote
    ○ Should ignore empty data and errors from local and emit from Remote
    ○ If Remote also fails, should show error

    View full-size slide

  18. Design the Flow
    https://www.rivu.dev/
    ● getArticles() should return from Local first and then after syncing should
    return from Remote
    ○ Should ignore empty data and errors from local and emit from Remote
    ○ If Remote also fails, should show error
    ● fetchAndSyncArticles() should fetch from Remote, save it in local and emit
    the data returned from Remote

    View full-size slide

  19. Break down of
    Steps
    ● Create Interface
    https://www.rivu.dev/
    interface ArticlesRepository {
    fun getArticles(): Flowable>
    fun fetchAndSyncArticles(): Single>
    }

    View full-size slide

  20. Break down of
    Steps
    ● Create Interface
    ● Create Blank
    Implementations
    https://www.rivu.dev/
    class ArticlesRepositoryImpl(
    private val localDataStore: ArticlesDataStore,
    private val remoteDataStore: ArticlesDataStore
    ) : ArticlesRepository {
    override fun getArticles(): Flowable> {
    TODO("Not yet Implemented")
    }
    override fun fetchAndSyncArticles(): Single> {
    TODO("Not yet Implemented")
    }
    }

    View full-size slide

  21. Break down of
    Steps
    ● Create Interface
    ● Create Blank
    Implementations
    https://www.rivu.dev/
    class ArticlesRepositoryImpl(
    private val localDataStore: ArticlesDataStore,
    private val remoteDataStore: ArticlesDataStore
    ) : ArticlesRepository {
    override fun getArticles(): Flowable> {
    TODO("Not yet Implemented")
    }
    override fun fetchAndSyncArticles(): Single> {
    TODO("Not yet Implemented")
    }
    }

    View full-size slide

  22. Break down of
    Steps
    ● Create Interface
    ● Create Blank
    Implementation
    ● Write Tests
    https://www.rivu.dev/
    Fakes
    class FakeSuccessDataStore(val data: List): ArticlesDataStore {
    override fun getArticles(): Single> = Single.defer {
    Single.just(data)
    }
    override fun saveArticles(articleList: List): Completable =
    Completable.defer {
    Completable.complete()
    }
    }
    class FakeSaveDataStore: ArticlesDataStore {
    val articles: MutableList = mutableListOf()
    override fun getArticles(): Single> = Single.defer {
    Single.just(emptyList())
    }
    override fun saveArticles(articleList: List): Completable =
    Completable.fromCallable {
    articles.clear()
    articles.addAll(articleList)
    }
    }
    class FakeEmptyDataStore: ArticlesDataStore {
    override fun getArticles(): Single> = Single.defer {
    Single.just(emptyList())
    }
    override fun saveArticles(articleList: List): Completable =
    Completable.defer {
    Completable.complete()
    }
    }
    class FakeErrorDataStore(val error: Exception): ArticlesDataStore {
    override fun getArticles(): Single> = Single.defer {

    View full-size slide

  23. Break down of
    Steps
    ● Create Interface
    ● Create Blank
    Implementation
    ● Write Tests
    https://www.rivu.dev/
    Fakes
    class FakeSuccessDataStore(val data: List): ArticlesDataStore {
    override fun getArticles(): Single> = Single.defer {
    Single.just(data)
    }
    override fun saveArticles(articleList: List): Completable =
    Completable.defer {
    Completable.complete()
    }
    }
    class FakeSaveDataStore: ArticlesDataStore {
    val articles: MutableList = mutableListOf()
    override fun getArticles(): Single> = Single.defer {
    Single.just(emptyList())
    }
    override fun saveArticles(articleList: List): Completable =
    Completable.fromCallable {
    articles.clear()
    articles.addAll(articleList)
    }
    }
    class FakeEmptyDataStore: ArticlesDataStore {
    override fun getArticles(): Single> = Single.defer {
    Single.just(emptyList())
    }
    override fun saveArticles(articleList: List): Completable =
    Completable.defer {
    Completable.complete()
    }
    }
    class FakeErrorDataStore(val error: Exception): ArticlesDataStore {
    override fun getArticles(): Single> = Single.defer {

    View full-size slide

  24. Break down of
    Steps
    ● Create Interface
    ● Create Blank
    Implementation
    ● Write Tests
    https://www.rivu.dev/
    Fakes
    class FakeSuccessDataStore(val data: List): ArticlesDataStore {
    override fun getArticles(): Single> = Single.defer {
    Single.just(data)
    }
    override fun saveArticles(articleList: List): Completable =
    Completable.defer {
    Completable.complete()
    }
    }
    class FakeSaveDataStore: ArticlesDataStore {
    val articles: MutableList = mutableListOf()
    override fun getArticles(): Single> = Single.defer {
    Single.just(emptyList())
    }
    override fun saveArticles(articleList: List): Completable =
    Completable.fromCallable {
    articles.clear()
    articles.addAll(articleList)
    }
    }
    class FakeEmptyDataStore: ArticlesDataStore {
    override fun getArticles(): Single> = Single.defer {
    Single.just(emptyList())
    }
    override fun saveArticles(articleList: List): Completable =
    Completable.defer {
    Completable.complete()
    }
    }
    class FakeErrorDataStore(val error: Exception): ArticlesDataStore {
    override fun getArticles(): Single> = Single.defer {

    View full-size slide

  25. Break down of
    Steps
    ● Create Interface
    ● Create Blank
    Implementation
    ● Write Tests
    https://www.rivu.dev/
    Fakes
    class FakeSaveDataStore: ArticlesDataStore {
    val articles: MutableList = mutableListOf()
    override fun getArticles(): Single> = Single.defer {
    Single.just(emptyList())
    }
    override fun saveArticles(articleList: List): Completable =
    Completable.fromCallable {
    articles.clear()
    articles.addAll(articleList)
    }
    }
    class FakeEmptyDataStore: ArticlesDataStore {
    override fun getArticles(): Single> = Single.defer {
    Single.just(emptyList())
    }
    override fun saveArticles(articleList: List): Completable =
    Completable.defer {
    Completable.complete()
    }
    }
    class FakeErrorDataStore(val error: Exception): ArticlesDataStore {
    override fun getArticles(): Single> = Single.defer {
    Single.error(error)
    }
    override fun saveArticles(articleList: List): Completable =
    Completable.defer {
    Completable.error(error)
    }
    }

    View full-size slide

  26. Break down of
    Steps
    ● Create Interface
    ● Create Blank
    Implementation
    ● Write Tests
    https://www.rivu.dev/
    fetchAndSyncArticles
    @Test
    fun `fetchAndSyncArticles should emit remote data`() {
    /*When remote returns data, save it to DB
    *and emit the same */
    //Setup
    val dummyData = TestDataFactory.dummyArticlesList
    val localDS = FakeSaveDataStore()
    val remoteDS = FakeSuccessDataStore(dummyData)
    val articlesRepo = ArticlesRepositoryImpl(localDS,
    remoteDS)
    //When
    val testObserver =
    articlesRepo.fetchAndSyncArticles().test()
    //Then
    testObserver.assertValue(dummyData)//emitted remote
    data
    assertEquals(dummyData, localDS.articles)//saved it
    to localDS
    }

    View full-size slide

  27. Break down of
    Steps
    ● Create Interface
    ● Create Blank
    Implementation
    ● Write Tests
    https://www.rivu.dev/
    getArticles
    @Test
    fun `getArticles should emit EmptyResultSetException`() {
    /*When both DS emits empty data
    * It should emit EmptyResultSetException */
    //Setup
    val localDS = FakeEmptyDataStore()
    val remoteDS = FakeEmptyDataStore()
    val articlesRepo = ArticlesRepositoryImpl(localDS,
    remoteDS)
    //When
    val testObserver = articlesRepo.getArticles().test()
    //Then
    testObserver.assertError(EmptyResultSetException::class.java)
    }

    View full-size slide

  28. Break down of
    Steps
    ● Create Interface
    ● Create Blank
    Implementation
    ● Write Tests
    https://www.rivu.dev/
    getArticles
    @Test
    fun `getArticles should emit both data`() {
    /* When both DS emits data
    * It should first emit localDS data and then from
    remoteDS */
    //Setup
    val dummyLocalData = TestDataFactory.dummyArticlesList
    val dummyRemoteData = TestDataFactory.dummyArticlesList
    val localDS = FakeSuccessDataStore(dummyLocalData)
    val remoteDS = FakeSuccessDataStore(dummyRemoteData)
    val articlesRepo = ArticlesRepositoryImpl(localDS,
    remoteDS)
    //When
    val testObserver = articlesRepo.getArticles().test()
    //Then
    testObserver.assertValueCount(2)
    testObserver.assertValues(dummyLocalData,
    dummyRemoteData)
    }

    View full-size slide

  29. Break down of
    Steps
    ● Create Interface
    ● Create Blank
    Implementation
    ● Write Tests
    ● Let them fail
    https://www.rivu.dev/

    View full-size slide

  30. Break down of
    Steps
    ● Create Interface
    ● Create Blank
    Implementation
    ● Write Tests
    ● Let them fail
    ● Write the code
    https://www.rivu.dev/
    class ArticlesRepositoryImpl (
    private val localDataStore: ArticlesDataStore,
    private val remoteDataStore: ArticlesDataStore
    ): ArticlesRepository {
    override fun fetchAndSyncArticles(): Single> {
    return remoteDataStore.getArticles()
    .flatMap {
    if (it.isNotEmpty()) {
    localDataStore.saveArticles(it)
    .andThen(Single.just(it))
    } else {
    Single.error(EmptyResultSetException(EMPTY_DATA_MESSAGE))
    }
    }
    }
    override fun getArticles(): Flowable> {
    return localDataStore.getArticles()
    .mergeWith(fetchAndSyncArticles())
    .onErrorResumeWith(fetchAndSyncArticles())
    }
    }

    View full-size slide

  31. Break down of
    Steps
    ● Create Interface
    ● Create Blank
    Implementation
    ● Write Tests
    ● Let them fail
    ● Write the code
    https://www.rivu.dev/
    class ArticlesRepositoryImpl (
    private val localDataStore: ArticlesDataStore,
    private val remoteDataStore: ArticlesDataStore
    ): ArticlesRepository {
    override fun fetchAndSyncArticles(): Single> {
    return remoteDataStore.getArticles()
    .flatMap {
    if (it.isNotEmpty()) {
    localDataStore.saveArticles(it)
    .andThen(Single.just(it))
    } else {
    Single.error(EmptyResultSetException(EMPTY_DATA_MESSAGE))
    }
    }
    }
    override fun getArticles(): Flowable> {
    return localDataStore.getArticles()
    .mergeWith(fetchAndSyncArticles())
    .onErrorResumeWith(fetchAndSyncArticles())
    }
    }

    View full-size slide

  32. Break down of
    Steps
    ● Create Interface
    ● Create Blank
    Implementation
    ● Write Tests
    ● Let them fail
    ● Write the code
    ● Run the tests again, see
    them pass 😎
    https://www.rivu.dev/

    View full-size slide

  33. Resources
    ● https://speakerdeck.com/rivuchk/android-worldwide-tdd-in-android this presentation
    ● http://agiledata.org/essays/tdd.html Intro to TDD
    ● https://www.raywenderlich.com/7109-test-driven-development-tutorial-for-android-getting-st
    arted TDD in Android Tutorial
    ● https://medium.com/mobility/how-to-do-tdd-in-android-90f013d91d7f TDD in Android
    Tutorial Series
    https://www.rivu.dev/

    View full-size slide

  34. Thanks!
    Rivu Chakraborty
    Twitter: @rivuchakraborty
    Website: https://rivu.dev

    View full-size slide