AUTO1 Group

Testing your isolated Fragments with Koin

By Oleg Osipenko

Senior Software Engineer

< Back to list
Coding Sep 9

UI-tests are a necessary part of our development routine in AUTO1. Although that wasn’t the case a while ago. One of the issues, which was blocking developers from writing and running UI-tests, was the need to set up UI-tests. Of course, to test the screen in isolation you need to have some architecture, employ the SOLID principles, especially Dependency inversion. With proper architecture it’s easy to substitute dependencies inside screen under the test with fakes or mocks. But even with good architecture you need to provide these mock implementations. And it’s not always obvious how to make your DI framework to provide mocks for tests.

Recently we started a brand new project from scratch. And we decided to give Koin a try. Why Koin and not Dagger? — Just because Koin offers concise and minimalistic syntax and it uses Kotlin. Also, important for us was the question of code generation. Although Google announced that in new versions of Android Gradle plug-in they made kapt incremental, we still didn’t want to add this extra burden to our build process. Simultaneously Koin 2.0 was announced at the same time. That’s how we found ourselves facing question: how to provide mock dependencies to our UI-tests?

Let’s start with fragment…

Inside our fragments we inject number of dependencies, typically they include viewmodels:

private val fragmentViewModel: EmailLoginFragmentViewModel by viewModel()

That’s exactly what we want to substitute with mocks. To do so we need a custom Application class, which will start Koin with our test module, not the real one.

class KoinTestApp: Application() {
  override fun onCreate() {
    super.onCreate()
    startKoin {
    androidLogger()
    androidContext(this@KoinTestApp)
    modules(emptyList())
    }
  }

  internal fun injectModule(module: Module) {
    loadKoinModules(module)
  }
}

We start our Koin instance inside onCreate() method, and we pass emptyList() of modules. We also have a method injectModule() which accepts our test module. Since we already started our application, we are using method loadKoinModules() to add this test module to the graph.

How to run custom application

But we need to somehow start our test application class. We cannot use AndroidManifest inside androidTest folder, because manifest merger will ignore it if we set our test application inside Manifest like this: <application name=".KoinTestApp">. So we could use a custom test runner for that purpose. AndroidJUnitRunner class has a newApplication() method. And we can pass our test application name there, making our test runner to start our test application instead of a real one.

class KoinTestRunner: AndroidJUnitRunner() {
  override fun newApplication(
    cl: ClassLoader?, 
    className: String?, 
    context: Context?): Application {
      return super.newApplication(cl, KoinTestApp::class.java.name, context)
  }
}

Then we need to change our build.gradle inside our app module:

android {
  defaultConfig {
    …
    …
    testInstrumentationRunner “path.to.your.package.KoinTestRunner”
  }
}

And don’t forget to disable animations for UI-tests:

android {
  testOptions {
    animationsDisabled = true
  }
}

Test rule for fragments

We are almost done. We only need to write our test and provide our test module to it. We are using our custom fragment test rule, which provides a couple of methods for submitting your fragment and mock test module. But you could use the same approach for activities.

abstract class FragmentTestRule<F: Fragment>: 
  ActivityTestRule<FragmentActivity>(FragmentActivity::class.java, true) {
  
  override fun afterActivityLaunched() {
    super.afterActivityLaunched()
    activity.runOnUiThread {
      val fm = activity.supportFragmentManager
      val transaction = fm.beginTransaction()
      transaction.replace(android.R.id.content, createFragment())
        .commit()
    }
  }
  
  override fun beforeActivityLaunched() {
    super.beforeActivityLaunched()
    val application = InstrumentationRegistry.getInstrumentation()
      .targetContext.applicationContext as KoinTestApp
    application.injectModule(getModule())
  }

  protected abstract fun createFragment(): F
  
  protected abstract fun getModule(): Module
  
  fun launch() {
    launchActivity(Intent())
  }
}

fun <F: Fragment> createRule(fragment: F, module: Module): FragmentTestRule<F> = 
  object: FragmentTestRule<F>() {
    override fun createFragment(): F = fragment

    override fun getModule(): Module = module
  }

I am using default Android FragmentActivity to host fragment under test. So we need to mention this activity in the manifest. For that purpose I created instance of AndroidManifest inside debug variant in my app module:

<?xml version="1.0" encoding="utf-8"?>
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.github.olegosipenko.kointestsample">
  <application>
    <activity
      android:theme="@style/AppTheme"
      android:name="androidx.fragment.app.FragmentActivity"/>
  </application>
</manifest>

Test itself

Finally, our test class could look like this:

@RunWith(AndroidJUnit4ClassRunner::class)
class EmailLoginFragmentTest {
  private val fragmentViewModel: EmailLoginFragmentViewModel = mockk(relaxed = true)
  private val fragment = EmailLoginFragment()

  @get:Rule
  val fragmentRule = createRule(fragment, module {
    single(override = true) {
      fragmentViewModel
    }
  })

  @Test
  fun testBasicInvocation() {
    onScreen<EmailLoginForm> {
      emailField { typeText(EMAIL) }
      passwordField { typeText(PASSWORD) }
      loginButton.click()
      verify {
        fragmentViewModel.loginWithCredentials(EMAIL, PASSWORD)
      }
    }
  }

  class EmailLoginForm: Screen<EmailLoginForm>() {
    val emailField = KEditText { withId(R.id.textFieldEmail) }
    val passwordField = KEditText { withId(R.id.textFieldPassword) }
    val loginButton = KButton { withId(R.id.buttonLogin) }
  }
}

private const val EMAIL = "some@email.com"
private const val PASSWORD = "password"

Here we are using Kakao, it’s a nice tool to simplify working with Espresso tests. It provides implementation of Page Object pattern and allows you to abstract interactions with your UI behind the abstract Screen objects. Using Kakao you can improve maintainability of your tests, so if you are not using it yet, give it a try. Also we are using Mockk for mocking, which uses all the power Kotlin gives us.

That’s how with Koin, Kakao and Mockk you could easily test your fragments in isolation, making your life easier.

You can check the source code for the sample here.

Stories you might like:
CodingJul 2
By Nicholas Peretti

Create forms at scale with Formik and Yup

CodingJun 24
By Wojciech Oroński

Yet another case study of developing serverless apps with PHP.

CodingApr 4
By Chirag Swadia

How we use ES6 generators instead of thunk to simplify our React Redux application code and...