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

TDD in Android with Spek :: droidcon APAC

TDD in Android with Spek :: droidcon APAC

Presented at droidcon APAC, 15th December 2020

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. While discussing on TDD, I'll also be discussing on writing test cases with Spek DSL and the different styles of DSL offered by Spek. I'll also be showing how to setup Spek.
We'll be writing a feature, following TDD and coding live in the session.
Spek being a DSL is descriptive way of writing rest cases, thus making them more readable and better organized, which will help further to understand TDD.
Moreover, if you're new in android-dev, but have prior experience in web, you'll find Spek more familiar than traditional JUnit4 unit testing.

Rivu Chakraborty

December 15, 2020
Tweet

More Decks by Rivu Chakraborty

Other Decks in Programming

Transcript

  1. Rivu Chakraborty • GDE for Kotlin • Android Architect @

    Paytm Insider • Community Person, Avid Learner • Author, Blogger, Speaker https://www.rivu.dev/
  2. Benefits of Spek • Easy to Read/Write Tests • Using

    DSL to Group Tests together • Built with Kotlin MP in Mind (Spek 2.0+) https://www.rivu.dev/
  3. Any Gotchas on Spek • A bit complex to Setup

    for Android • Sometimes Unreliable ◦ But it’s improving with each release. https://www.rivu.dev/
  4. My Recommendations on Spek • Can definitely use in your

    Side Projects / Demo apps • Wait a little till 2.1.0 becomes stable to use in your prod app https://www.rivu.dev/
  5. Setting up Spek https://www.rivu.dev/ buildscript { ext.kotlin_version = "1.4.10" repositories

    { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.2.0-alpha16' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "de.mannodermaus.gradle.plugins:android-junit5:$android_junit5_version" classpath "org.jacoco:org.jacoco.core:$jacoco_version" } }
  6. Setting up Spek https://www.rivu.dev/ allprojects { repositories { google() jcenter()

    maven { url "https://dl.bintray.com/spekframework/spek-dev" } } }
  7. Writing Tests with Spek https://www.rivu.dev/ class RemoteMovieDataStoreSpecTest : Spek({ describe("Group

    of Tests") { context("Sub group 1") { it("test 1") { assert(true) } it("test 2") { assert(true) } } context("Sub group 2") { it("test 1") { assert(true) } } } }) Detailed post on Spek: https://www.rivu.dev/unit-testing-in-android-with-spek/
  8. Test Driven Development • Write Tests, let them fail and

    then write Implementation • We want Tests to fail first https://www.rivu.dev/
  9. 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/
  10. 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/
  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 ◦ Implement the methods with TODO (We still want them to compile ) 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 ) ◦ Write the Tests and let them fail 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 Implementation, make sure the tests pass now https://www.rivu.dev/
  14. Write the Interface interface ArticlesRepository { fun getArticles(): Flowable<List<Article>> fun

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

    { fun getArticles(): Flowable<List<Article>> fun fetchAndSyncArticles(): Single<List<Article>> }
  18. 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") } }
  19. 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") } }
  20. 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 {
  21. 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 {
  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 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) } }
  24. Break down of Steps • Create Interface • Create Blank

    Implementation • Write Tests https://www.rivu.dev/ fetchAndSyncArticles describe("test fetchAndSyncArticles scenarios") { context("remoteDS returns data") { val dummyData = TestDataFactory.dummyArticlesList beforeEachGroup { localDS = FakeSaveDataStore() remoteDS = FakeSuccessDataStore(dummyData) articlesRepo = ArticlesRepositoryImpl(localDS, remoteDS) } it("should emit remote data, and save it to local") { val testObserver = articlesRepo.fetchAndSyncArticles().test() testObserver.assertValue(dummyData) assertEquals(dummyData, (localDS as FakeSaveDataStore).articles) } } }
  25. Break down of Steps • Create Interface • Create Blank

    Implementation • Write Tests https://www.rivu.dev/ getArticles describe("test getArticles scenarios") { context("both DS emits empty") { beforeEachGroup { localDS = FakeEmptyDataStore() remoteDS = FakeEmptyDataStore() articlesRepo = ArticlesRepositoryImpl(localDS, remoteDS) } it("should emit EmptyResultSetException") { val testObserver = articlesRepo.getArticles().test() testObserver.assertError(EmptyResultSetException::class.java) } } context("both DS emits data") { val dummyLocalData = TestDataFactory.dummyArticlesList val dummyRemoteData = TestDataFactory.dummyArticlesList beforeEachGroup { localDS = FakeSuccessDataStore(dummyLocalData) remoteDS = FakeSuccessDataStore(dummyRemoteData) articlesRepo = ArticlesRepositoryImpl(localDS, remoteDS) } it("should emit both data") {
  26. Break down of Steps • Create Interface • Create Blank

    Implementation • Write Tests https://www.rivu.dev/ getArticles it("should emit EmptyResultSetException") { val testObserver = articlesRepo.getArticles().test() testObserver.assertError(EmptyResultSetException::class.java) } } context("both DS emits data") { val dummyLocalData = TestDataFactory.dummyArticlesList val dummyRemoteData = TestDataFactory.dummyArticlesList beforeEachGroup { localDS = FakeSuccessDataStore(dummyLocalData) remoteDS = FakeSuccessDataStore(dummyRemoteData) articlesRepo = ArticlesRepositoryImpl(localDS, remoteDS) } it("should emit both data") { val testObserver = articlesRepo.getArticles().test() testObserver.assertValueCount(2) testObserver.assertValues(dummyLocalData, dummyRemoteData) testObserver.assertNoErrors() } } }
  27. Break down of Steps • Create Interface • Create Blank

    Implementation • Write Tests • Let them fail https://www.rivu.dev/
  28. 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()) } }
  29. 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()) } }
  30. 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/
  31. Resources • https://www.rivu.dev/unit-testing-in-android-with-spek/ For Spek • https://github.com/RivuChk/Jetpack-Compose-MVI-Demo Demo app with

    Spek and Compose • http://agiledata.org/essays/tdd.html Intro to TDD • https://www.raywenderlich.com/7109-test-driven-development-tutorial-for-android-getting-started TDD in Android Tutorial • https://caster.io/courses/espressotdd TDD with Espresso (Caster.io Screencast) • https://medium.com/mobility/how-to-do-tdd-in-android-90f013d91d7f TDD in Android Tutorial Series https://www.rivu.dev/