0

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.

Archie G. Quiñones
  • 11,638
  • 18
  • 65
  • 107

1 Answers1

3

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

El Paco
  • 546
  • 2
  • 6
  • This is testing the Dependencies but not the activity itself though. And yes although technically testing activity isnt UnitTesting (as it has dependencies with android itself), but we probably want to test Activity in isolation for example when a button is clicked, does it call the correct function or parameter, and this is very much doable. I'm not really sure if I understand everything correctly. – Archie G. Quiñones Mar 24 '20 at 15:24
  • 1
    My advice is to decouple from the Android framework, so what you test is the presentation layer instead. When you do an action, you don't trigger the UI directly, you create a new state. The presentation layer then translates from that state to clicking on the UI. The same on the other direction. That way your tests become testing transitions between states when a new input happens. See the example on 'Headless development' of how I test a login flow end to end using only Observables. – El Paco Mar 24 '20 at 15:50
  • If you want to test that your presentation layer clicks on the real button on the UI or you skip the presentation layer altogether, then you're in E2E territory, which is a whole different set of test runners and assertions. And it's historically painful to make them stable. – El Paco Mar 24 '20 at 15:51