Save 37% off PRO during our Black Friday Sale! »

droidcon Berlin :: TDD in Android with Spek

droidcon Berlin :: TDD in Android with Spek

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.

36c29634c5d55eae66224c24ba2b933c?s=128

Rivu Chakraborty

October 21, 2021
Tweet

Transcript

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

  2. Rivu Chakraborty • GDE for Kotlin • Product Engineer @

    Gojek • Worked with 2 biggest Indian Startups • Community Person, Avid Learner • Author, Blogger, Speaker https://www.rivu.dev/
  3. Why Spek?? https://www.rivu.dev/

  4. 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/
  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. Setting up Spek https://www.rivu.dev/ testImplementation "org.spekframework.spek2:spek-dsl-jvm:$spek_version" testImplementation "org.spekframework.spek2:spek-runner-junit5:$spek_version" Official Guide:

    https://www.spekframework.org/setup-android/
  8. 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/
  9. Types of Tests https://www.rivu.dev/ https://www.rivu.dev/

  10. Test Pyramid Unit Tests Integration Tests E2E Tests

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

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

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

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

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

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

    { fun getArticles(): Flowable<List<Article>> fun fetchAndSyncArticles(): Single<List<Article>> }
  24. 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") } }
  25. 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") } }
  26. 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 {
  27. 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 {
  28. 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 {
  29. 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) } }
  30. 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) } } }
  31. 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") {
  32. 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() } } }
  33. Break down of Steps • Create Interface • Create Blank

    Implementation • Write Tests • Let them fail https://www.rivu.dev/
  34. 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()) } }
  35. 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()) } }
  36. 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/
  37. Any Gotchas on Spek • A bit complex to Setup

    for Android • Unreliable https://www.rivu.dev/
  38. 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 ◦ Or use JUnit5 to setup own testing DSL https://www.rivu.dev/
  39. 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/
  40. Thanks! Rivu Chakraborty Twitter: @rivuchakraborty Website: https://rivu.dev