Search code examples
androiddependency-injectionarrow-kt

Dependency Injection on Arrow KT


In Arrow Kt Documentation on Dependency Injection, the dependency is defined at the "Edge of the World" or in Android could be an Activity or a Fragment. So the given example is as follow:

import Api.*

class SettingsActivity: Activity {
  val deps = FetcherDependencies(Either.monadError(), ActivityApiService(this))

  override fun onResume() {
    val id = deps.createId("1234")

    user.text =
      id.fix().map { it.toString() }.getOrElse { "" }

    friends.text =
      deps.getUserFriends(id).fix().getOrElse { emptyList() }.joinToString()
  }
}

But now I'm thinking how could the SettingsActivity in the example could be unit tested? Since the dependency is created within the activity, it could no longer be changed for testing?

When using some other Dependency Injection library, this dependency definition is create outside of the class it will be used on. For example in Dagger, a Module class is created to define how the objects (dependencies) are created and an @Inject is used to "inject" the dependency defined inside the module. So now when unit testing the Activity, I just have to define a different module or manually set the value of the dependency to a mock object.


Solution

  • In Dagger you would create a Mock or Test class that you would @Inject instead of ActivityApiService. It is the same here.

    Instead of:

    class ActivityApiService(val ctx: Context) {
      fun createId(): String = doOtherThing(ctx)
    }
    

    You do

    interface ActivityApiService {
      fun createId(): String
    }
    

    and now you have 2 implementations, one for prod

    class ActivityApiServiceImpl(val ctx: Context): ActivityApiService {
      override fun createId(): Unit = doOtherThing(ctx)
    }
    

    and another for testing

    fun testBla() {
      val api =  object: ActivityApiService {
        override fun createId(): String = "4321"
      }
    
      val deps = FetcherDependencies(Either.monadError(), api)
    
      deps.createId("1234") shouldBe "4321"
    }
    

    or even use Mockito or a similar tool to create an ActivityApiService.

    I have a couple of articles on how to decouple and unitest outside the Android framework that aren't Arrow-related. Check 'Headless development in Fully Reactive Apps' and the related project https://github.com/pakoito/FunctionalAndroidReference.

    If your dependency graph becomes too entangled and you'd like some compile-time magic to create those dependencies, you can always create a local class in tests and @Inject the constructor there. The point is to decouple from things that aren't unitestable, like the whole Android framework :D