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. TDD in Android with Spek
    Rivu Chakraborty
    https://www.rivu.dev/

    View Slide

  2. Rivu Chakraborty
    ● GDE for Kotlin
    ● Android Architect @ Paytm Insider
    ● Community Person, Avid Learner
    ● Author, Blogger, Speaker
    https://www.rivu.dev/

    View Slide

  3. Why Spek??
    https://www.rivu.dev/

    View Slide

  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/

    View Slide

  5. Any Gotchas on Spek
    ● A bit complex to Setup for Android
    ● Sometimes Unreliable
    ○ But it’s improving with each release.
    https://www.rivu.dev/

    View Slide

  6. 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/

    View Slide

  7. 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"
    }
    }

    View Slide

  8. Setting up Spek
    https://www.rivu.dev/
    allprojects {
    repositories {
    google()
    jcenter()
    maven { url "https://dl.bintray.com/spekframework/spek-dev" }
    }
    }

    View Slide

  9. 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/

    View Slide

  10. 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/

    View Slide

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

    View Slide

  12. Test Pyramid
    Unit Tests
    Integration Tests
    E2E Tests

    View Slide

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

    View Slide

  14. Test Driven Development
    ● Write Tests, let them fail and then write Implementation

    View Slide

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

    View Slide

  16. 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 Slide

  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
    https://www.rivu.dev/

    View Slide

  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 )
    https://www.rivu.dev/

    View Slide

  19. 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 Slide

  20. 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 Slide

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

    View Slide

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

    View Slide

  23. 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 Slide

  24. 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 Slide

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

    View Slide

  26. 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 Slide

  27. 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 Slide

  28. 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 Slide

  29. 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 Slide

  30. 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 Slide

  31. 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 Slide

  32. 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)
    }
    }
    }

    View Slide

  33. 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") {

    View Slide

  34. 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()
    }
    }
    }

    View Slide

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

    View Slide

  36. 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 Slide

  37. 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 Slide

  38. 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 Slide

  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/

    View Slide

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

    View Slide