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.

36c29634c5d55eae66224c24ba2b933c?s=128

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

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

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

  4. Test Pyramid Unit Tests Integration Tests E2E Tests

  5. Unit Tests @Test fun `test getUsers`() = runBlocking { val

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

    expected = localDS.getUsers() //Actual Local DS val result = testUserRepository.getUsers().first() assertEquals(expected, result) }
  7. What is TDD???? https://www.rivu.dev/ https://www.rivu.dev/

  8. Test Driven Development • Write Tests, let them fail and

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

    then write Implementation • We want Tests to fail first https://www.rivu.dev/
  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/
  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/
  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/
  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/
  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/
  15. Let’s do it https://www.rivu.dev/ https://www.rivu.dev/

  16. Write the Interface interface ArticlesRepository { fun getArticles(): Flowable<List<Article>> fun

    fetchAndSyncArticles(): Single<List<Article>> } https://www.rivu.dev/
  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
  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
  19. Break down of Steps • Create Interface https://www.rivu.dev/ interface ArticlesRepository

    { fun getArticles(): Flowable<List<Article>> fun fetchAndSyncArticles(): Single<List<Article>> }
  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<List<Article>> { TODO("Not yet Implemented") } override fun fetchAndSyncArticles(): Single<List<Article>> { TODO("Not yet Implemented") } }
  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<List<Article>> { TODO("Not yet Implemented") } override fun fetchAndSyncArticles(): Single<List<Article>> { TODO("Not yet Implemented") } }
  22. Break down of Steps • Create Interface • Create Blank

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

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

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

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

    Implementation • Write Tests • Let them fail https://www.rivu.dev/
  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<List<Article>> { 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<List<Article>> { return localDataStore.getArticles() .mergeWith(fetchAndSyncArticles()) .onErrorResumeWith(fetchAndSyncArticles()) } }
  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<List<Article>> { 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<List<Article>> { return localDataStore.getArticles() .mergeWith(fetchAndSyncArticles()) .onErrorResumeWith(fetchAndSyncArticles()) } }
  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/
  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/
  34. Thanks! Rivu Chakraborty Twitter: @rivuchakraborty Website: https://rivu.dev