ant2009
ant2009

Reputation: 22566

Injecting a dependency for espresso rule for an instrumentation test

android studio 3.4.1
dagger-android 2.21

I am using dagger-android to inject my OKHttpClient into a espresso rule. But haven't found a way to do this, I have attempted many differnt things.

This is my rule I am using and I am trying to inject the okHttpClient into it

class OkHttpIdingResourceRule(application: Application) : TestRule {

    /* My attempt below - but not working */
    private val testApplication =
        InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
                as AndroidTestGoWeatherApplication

    // private val testApplication = application.applicationContext as AndroidTestGoWeatherApplication
    private val component = testApplication.component as AndroidTestGoWeatherPresentationComponent
    private val okHttpClient: OkHttpClient = component.okHttpClient()

    private val idlingResource: IdlingResource = OkHttp3IdlingResource.create("okhttp", okHttpClient)

    override fun apply(base: Statement?, description: Description?): Statement {
        return object: Statement() {
            override fun evaluate() {
                IdlingRegistry.getInstance().register(idlingResource)
                base?.evaluate()
                IdlingRegistry.getInstance().unregister(idlingResource)
            }
        }
    }
}

This is my AndroidTestGoWeatherApplication

class AndroidTestGoWeatherApplication : GoWeatherApplication(), HasActivityInjector {
    @Inject
    lateinit var activityInjector: DispatchingAndroidInjector<Activity>

    override fun activityInjector(): AndroidInjector<Activity> = activityInjector
}

My Application

open class GoWeatherApplication : Application(), HasActivityInjector, HasSupportFragmentInjector, HasServiceInjector {

    @Inject
    lateinit var dispatchingAndroidActivityInjector: DispatchingAndroidInjector<Activity>

    @Inject
    lateinit var dispatchingAndroidFragmentInjector: DispatchingAndroidInjector<Fragment>

    @Inject
    lateinit var dispatchingAndroidServiceInjector: DispatchingAndroidInjector<Service>

    lateinit var component: GoWeatherComponent

    override fun onCreate() {
        super.onCreate()

        component = DaggerGoWeatherComponent
            .builder()
            .application(this)
            .build()

            component.inject(this)
    }

    override fun activityInjector(): AndroidInjector<Activity> {
        return dispatchingAndroidActivityInjector
    }

    override fun supportFragmentInjector(): AndroidInjector<Fragment> {
        return dispatchingAndroidFragmentInjector
    }

    override fun serviceInjector(): AndroidInjector<Service> {
        return dispatchingAndroidServiceInjector
    }
}

My main application Component

GoWeatherComponent
@Singleton
@Component(modules = [
    AndroidSupportInjectionModule::class,
    ActivityBuilder::class,
    NetworkModule::class,
    GoWeatherApplicationModule::class])
interface GoWeatherComponent {
    @Component.Builder
    interface Builder {
        @BindsInstance
        fun application(application: GoWeatherApplication): Builder

        fun build(): GoWeatherComponent
    }

    fun inject(application: GoWeatherApplication)
}

My test application component

@Singleton
@Component(modules = [
    AndroidSupportInjectionModule::class,
    TestNetworkModule::class,
    TestGoWeatherApplicationModule::class,
    TestForecastModule::class])
interface AndroidTestGoWeatherPresentationComponent : AndroidInjector<AndroidTestGoWeatherApplication> {

    @Component.Builder
    abstract class Builder : AndroidInjector.Builder<AndroidTestGoWeatherApplication>() {
        abstract fun applicationModule(TestApplicationModule: TestGoWeatherApplicationModule): Builder

        abstract fun testNetworkModule(testNetworkModule: TestNetworkModule): Builder
    }

    fun okHttpClient(): OkHttpClient
}

This is my TestNetworkModule where I am creating my OkHttpClient

@Module
class TestNetworkModule {

    @Singleton
    @Provides
    fun httpLoggingInterceptor(): HttpLoggingInterceptor {
        val loggingInterceptor = HttpLoggingInterceptor()

        loggingInterceptor.level = if(BuildConfig.DEBUG) {
            HttpLoggingInterceptor.Level.BODY
        }
        else {
            HttpLoggingInterceptor.Level.NONE
        }

        return loggingInterceptor
    }

    @Singleton
    @Provides
    fun provideOkHttpClient(httpLoggingInterceptor: HttpLoggingInterceptor): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(httpLoggingInterceptor)
            .connectTimeout(2, TimeUnit.SECONDS)
            .readTimeout(2, TimeUnit.SECONDS)
            .build()
    }

    @Named("TestBaseUrl")
    @Singleton
    @Provides
    fun provideBaseUrlTest(): String =
        "http://localhost:8080/"

    @Singleton
    @Provides
    fun provideRetrofit(@Named("TestBaseUrl") baseUrl: String, okHttpClient: OkHttpClient?): Retrofit {
        return Retrofit.Builder()
            .baseUrl(baseUrl)
            .client(okHttpClient!!)
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build()
    }
}

My ActivityBuilder

@Module
abstract class ActivityBuilder {
    @ContributesAndroidInjector(modules = [ActivityModule::class])
    abstract fun injectIntoHomeActivity(): ForecastActivity

    @ContributesAndroidInjector(modules = [ActivityModule::class, ForecastModule::class])
    abstract fun injectIntoForecastFragment(): ForecastFragment
}

My main Activity

class ForecastActivity : AppCompatActivity(), ForecastView, RetryListener, LocationUtilsListener {

    companion object {
        const val WEATHER_FORECAST_KEY = "weatherForecast"
    }

    @Inject
    lateinit var forecastPresenter: ForecastPresenter

    @Inject
    lateinit var location: LocationUtils

    private var fragmentManager: FragmentManager? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_home)
    }
}

My instrumention test

@RunWith(AndroidJUnit4::class)
class ForecastActivityAndroidTest {
    @Inject
    lateinit var okHttpClient: OkHttpClient

    @get:Rule
    val okHttpIdingResourceRule = OkHttpIdingResourceRule(InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as AndroidTestGoWeatherApplication)

    @get:Rule
    val activityRule = ActivityTestRule(ForecastActivity::class.java, false, false)

    private val mockWebserver: MockWebServer by lazy {
        MockWebServer()
    }

    @Before
    fun setUp() {
        val testApplication =
            InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
                    as AndroidTestGoWeatherApplication

        DaggerAndroidTestGoWeatherPresentationComponent
            .builder()
            .applicationModule(TestGoWeatherApplicationModule())
            .create(testApplication)
            .inject(testApplication)

        mockWebserver.start(8080)
    }

    @After
    fun tearDown() {
        mockWebserver.shutdown()
    }

    @Test
    fun should_load_five_day_forecast() {
        loadFromResources("json/fivedayforecast.json")
        mockWebserver.enqueue(MockResponse().setBody(loadFromResources("json/fivedayforecast.json")))

        ActivityScenario.launch(ForecastActivity::class.java)

       /* do some testing here * 
    }
}

Many thanks in advance

Upvotes: 4

Views: 1304

Answers (1)

luis_cortes
luis_cortes

Reputation: 736

I think you're going about the process of injecting the OkHttpClient dependency into the OkHttpIdingResourceRule the wrong way.

From the dagger 2 docs:

Constructor injection is preferred whenever possible...

You own OkHttpIdingResourceRule so you should really be doing constructor injection here.

Allow Dagger to construct OkHttpIdingResourceRule for you by changing the constructor to look like this:

class OkHttpIdingResourceRule @Inject constructor(application: Application, okHttpClient: OkHttpClient)

Since OkHttpClient is already in your object graph I would inject OkHttpIdingResourceRule into your test instead of OkHttpClient

All this being said, I think you still have some issues with other parts of the code, but without running it and seeing the errors for myself I can't confirm them. For example, if you plan on injecting this test in this manner then you'll have to have a method like this on your test component:

void inject(ForecastActivityAndroidTest test);

Edit:

I've looked at your rule again and it looks like what you're really interested in is injecting the IdlingResource. If this is the case, you should change the constructor to look like this:

class OkHttpIdingResourceRule @Inject constructor(idlingRes: IdlingResource)

From there what you can do is create a provision method in your TestNetworkModule that creates it for you:

@Provides 
IdlingResource providesIdlingResource(OkHttpClient okHttpClient {
    return OkHttp3IdlingResource.create("okhttp", okHttpClient)
}

Upvotes: 2

Related Questions