Norway


Dependency injection is a great tool to break up your dependency creation into reusable pieces.
This helps separate your code into logical chunks where some create the dependencies and others
consume them. This separation helps in your unit because you have a clear list of
dependencies for each object so you can easily pass in mock versions.

In this post I will examine the use of dependency injection in integration tests with Espresso.
Some components, such as a network or database layer, need to be swapped out with fake versions for
faster tests that are sealed off from the outside. A good dependency injection setup can provide a
path for you to change the components you provide in your test environment. The sample project
(linked at the end of this post) uses Dagger 2.1 along with the Dagger-Android libraries to
simplify the setup.

Production Setup

This post will start with the main Dagger setup code before moving over to the test setup. The
configuration is fairly standard, so not much time will be spent covering how it works in detail.

First, a Module is created to provide the dependencies the needs. In this case, the module
provides a NerdStore that tracks a list of nerds. The function at the top of the module is the
main provider function. The second one at the bottom is an implementation detail that just provides
the dependencies the nerd store needs. Implementing modules in this way provides a nice separation
of concerns and ensures that if you want to replace your modules in test you only need to provide a
single dependency per module since none of the other dependencies are used elsewhere in the project.

@Module
class AppModule {

    @Provides
    @Singleton
    fun provideNerdStore(nerds: List<Nerd>): NerdStore {
        return LiveNerdStore(nerds)
    }

    // Implementation-detail provides functions below

    @Provides
    fun provideNerds(): List<Nerd> {
        return listOf(
                Nerd("Brian"),
                Nerd("Kristin"),
                Nerd("Chris")
        )
    }
}

Next, another Module is created to specify the classes that need the dependencies. Leaning on the
@ContributesAndroidInjector annotation here simplifies this file so separate subcomponents do not
need to be created for each Activity or added to a DispatchingAndroidInjector.

@Module
abstract class InjectorModule {

    @ContributesAndroidInjector
    abstract fun contributeNerdListActivityInjector(): NerdListActivity
}

With the modules out of the way, creating a component interface to orchestrate the injection is
next. The modules are listed in the @Component annotation alongside the
AndroidSupportInjectionModule. For this example, a plain Builder class annotated with
@Component.Builder is used so the setup is simplified.

@Singleton
@Component(modules = [
    AndroidSupportInjectionModule::class,
    InjectorModule::class,
    AppModule::class
])
interface AppComponent : AndroidInjector<NerdApplication> {
    @Component.Builder
    abstract class Builder : AndroidInjector.Builder<NerdApplication>()
}

Last but not least, an application subclass is needed to initialize the component. For simplicity,
the DaggerApplication class is extended and an override of the applicationInjector function is
implemented to return the generated DaggerAppComponent class. In this example, the component
class could be initialized in the applicationInjector function and returned. Instead, the
component is assigned to a property and initialized in onCreate. This is because the test
configuration will need to reassign the injector property in the tests to control which
dependencies are provided.

open class NerdApplication : DaggerApplication() {

    lateinit var injector: AndroidInjector<out DaggerApplication>

    override fun onCreate() {
        injector = DaggerAppComponent.builder().create(this)
        super.onCreate()
    }

    override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
        return injector
    }
}

With the NerdApplication class in place (and after registering it in the AndroidManifest.xml),
the setup is complete and the classes can request their dependencies. In the listing below, the
NerdListActivity class extends DaggerAppCompatActivity which automatically injects any
properties annotated with @Inject. In this case, the NerdStore property is annotated so the
component will create and inject an instance of it into the property.

class NerdListActivity : DaggerAppCompatActivity() {

    @Inject
    lateinit var nerdStore: NerdStore

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_nerd_list)
        for(nerd in nerdStore.nerds) {
            Log.d("NerdListActivity", "Have nerd $nerd")
        }
    }
}

Test Setup

With the production code in place, the testing harness can be set up. There is a fake version of the
LiveNerdStore that should be provided in the test classes, meaning a module needs to be created
to provide it as well as a test component to serve the module.

class OtherNerdStore(override val nerds: List<Nerd>)
    : NerdStore

The production modules and component do not need to be overridden, as long as the test component
provides all the dependencies the app needs. In this case, a test-specific module is created that
returns the OtherNerdStore class in place of the LiveNerdStore. Since the provide function only
specifies that a NerdStore is needed, this swap satisfies the application.

@Module
class FakeAppModule {
    @Provides
    fun provideNerdStore(): NerdStore {
        return OtherNerdStore(listOf())
    }
}

After that, a test component is created to use in the test classes. The InjectionModule can be
reused since there are no additional injections needed. If there were, another
TestInjectionModule could be added that would list all of the test classes that need dependencies
injected.

@Component(modules = [
    AndroidSupportInjectionModule::class,
    InjectorModule::class,
    FakeAppModule::class // Module that returns OtherNerdStore
])
interface TestComponent : AndroidInjector<NerdApplication> {
    @Component.Builder
    abstract class Builder : AndroidInjector.Builder<NerdApplication>()
}

Once the component is in place, the NerdApplication needs to be updated so the new component can
be set on it and used for the injection. Since this should only be accessible from the test
directory, this can be achieved by creating an extension function for NerdApplication in the test
directory to set the new component and setup the injection again.

fun NerdApplication.setApplicationInjector(injector: AndroidInjector<NerdApplication>) {
    this.injector = injector.also {
        it.inject(this)
    }
}

This extension function accepts an AndroidInjector class and sets it on the injector property on
NerdApplication. At the same time, the inject() function must be called on the injector,
passing in the application class as a parameter. This is necessary in order for the application to
use the new injector in the future.

One last bit of prep will help clean up the test code. When an Espresso test needs to start an
Activity, the ActivityTestRule is used to specify which class it should start. There is a
function on the test rule class called beforeActivityLaunched() that can be overridden. This is
where the production component will be swapped out for a test version before the activity launches.
This ensures that the activity uses the test component instead of the production one. To centralize
this, a subclass of ActivityTestRule is created that overrides the beforeActivityLaunched()
function.

class InjectionActivityTestRule<T : Activity>(
        activityClass: Class<T>,
        private val componentBuilder: AndroidInjector.Builder<NerdApplication>
) : ActivityTestRule<T>(activityClass) {

    override fun beforeActivityLaunched() {
        super.beforeActivityLaunched()
        // setup test component before activity launches
        val app = InstrumentationRegistry.getTargetContext().applicationContext as NerdApplication
        val testComponent = componentBuilder.create(app)
        app.setApplicationInjector(testComponent)
    }
}

This class takes in the activity class to start so it can pass it to the superclass
constructor. It also takes in a AndroidInjector.Builder class so it knows which component to set
on the application.

Then, in the beforeActivityLaunched() function, it accesses the application context from the
InstrumentationRegistry and casts it directly to NerdApplication. With the application
class, the test component can be fully created by calling create(app) on the builder. Finally,
the injector is configured on the application class by calling setApplicationInjector() and
passing in the test component as a parameter.

Having the extension function in the test directory ensures that the production code cannot call
it. This safety is not extended to the test classes. It is possible for a test class to call the
setApplicationInjector() function which could cause problems with the dependencies getting out
of sync.

For extra , extension function can be relocated into the InjectionActivityTestRule
file and marked as private. This ensures that it is only accessible within that file so the test
classes will be unable to modify the component.

class InjectionActivityTestRule<T : Activity>(
        activityClass: Class<T>,
        private val componentBuilder: AndroidInjector.Builder<NerdApplication>
) : ActivityTestRule<T>(activityClass) {

    override fun beforeActivityLaunched() {
        super.beforeActivityLaunched()
        // setup test component before activity launches
        val app = InstrumentationRegistry.getTargetContext().applicationContext as NerdApplication
        val testComponent = componentBuilder.create(app)
        app.setApplicationInjector(testComponent)
    }
}

private fun NerdApplication.setApplicationInjector(injector: AndroidInjector<NerdApplication>) {
    this.injector = injector.also {
        it.inject(this)
    }
}

With the InjectionActivityTestRule in place, the Espresso test can be written. In this case, the
test verifies that the injected property on the NerdActivity class is the fake version,
OtherNerdStore, instead of the real LiveNerdStore.

@RunWith(AndroidJUnit4::class)
class NerdListActivityTest {

    @get:Rule
    val activityTestRule = InjectionActivityTestRule(
            NerdListActivity::class.java,
            DaggerTestComponent.builder()
    )

    @Test
    fun itInjectsTheCorrectNerdStoreImplementation() {
        val activity = activityTestRule.activity
        val nerdStore = activity.nerdStore
        assertThat(nerdStore, instanceOf(OtherNerdStore::class.java))
    }
}

Running this results in a passed test and a confirmation that the test component is being used to
provide the dependencies instead of the production version.

Even though this setup is somewhat complex, I’m a fan because it gives you a higher level of
control over the tests. Each test class can specify its own test component to provide the app
dependencies. Some test classes are fine with a higher level of faked dependencies than others so
you have more say in your test setup.

The component is also recreated before each test because the activity is recreated on each test
run. This may slow down the execution time but it can have huge benefits by ensuring your tests run
in an isolated fashion. All dependencies are created fresh when the component is recreated so even
your singletons will not bleed state. You may still run into issues if your singletons point to the
same files when saving data, but that can be remedied by providing unique file names to write to in
each test or replacing your data caching with a fake, in-memory version.

I hope you have enjoyed my post on setting up dependency injection in tests. You can find a working
example of the code from this post in my
Github Repo. If you have any questions or
comments, I would love to hear from you. Feel free to leave a comment below or you can reach out to
me on Twitter @BrianGardnerAtl. Thanks for reading!





Source link

LEAVE A REPLY

Please enter your comment!
Please enter your name here