简体   繁体   中英

(DAGGER-ANDROID) Can not use @Inject on an Espresso Test and can not use mockWebServer

I'm trying to create Espresso tests and using a mockWebServer the thing is when I try to create my mockWebServer it calls the real api call and I want to intercept it and mock the response.

My dagger organisation is:

My App

open class App : Application(), HasAndroidInjector {

    lateinit var application: Application

    @Inject
    lateinit var androidInjector: DispatchingAndroidInjector<Any>

    override fun androidInjector(): AndroidInjector<Any> = androidInjector

    override fun onCreate() {
        super.onCreate()
        DaggerAppComponent.factory()
            .create(this)
            .inject(this)
        this.application = this
    }
}

Then MyAppComponent

@Singleton
@Component(
    modules = [
        AndroidInjectionModule::class,
        AppModule::class,
        RetrofitModule::class,
        RoomModule::class,
        AppFeaturesModule::class
    ]
)
interface AppComponent : AndroidInjector<App> {

    @Component.Factory
    interface Factory {
        fun create(@BindsInstance application: App): AppComponent
    }
}

Then I've created this TestApp

class TestApp : App() {

    override fun androidInjector(): AndroidInjector<Any> = androidInjector

    override fun onCreate() {
        DaggerTestAppComponent.factory()
            .create(this)
            .inject(this)
    }
}

And this is my TestAppComponent

@Singleton
@Component(
    modules = [
        AndroidInjectionModule::class,
        AppModule::class,
        TestRetrofitModule::class,
        AppFeaturesModule::class,
        RoomModule::class]
)
interface TestAppComponent : AppComponent {
    @Component.Factory
    interface Factory {
        fun create(@BindsInstance application: App): TestAppComponent
    }
}

Note: Here I've created a new module, called TestRetrofitModule where the BASE_URL is " http://localhost:8080 ", I don't know if I need something else.

Also I've created the TestRunner

class TestRunner : AndroidJUnitRunner() {

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

}

And put it on the testInstrumentationRunner

Problem 1

I can not use

@Inject
lateinit var okHttpClient: OkHttpClient

because it says that it's not initialised.

Problem 2 (Solved thanks Skizo)

My mockWebServer is not dispatching the responses even-though is not pointing the real api call, is pointing the one that I've put to the TestRetrofitModule, the thing is that I have to link that mockWebServer and Retrofit.

The setup you posted looks correct. As for App not being provided, you probably need to bind it in your component, since right now you're binding TestApp only. So you need to replace

fun create(@BindsInstance application: TestApp): TestAppComponent

with

fun create(@BindsInstance application: App): TestAppComponent

I had the same problem with mockWebServer recently, what you need to do is to put a breakpoint and see what's the error, in my case I put it on my BaseRepository where I was doing the call, and found that the exception was:

java.net.UnknownServiceException: CLEARTEXT communication to localhost not permitted by network security policy

What I did to solve the problem is add this on my manifest.xml

android:usesCleartextTraffic="true"

But you may have to use other approaches you can take a look on android-8-cleartext-http-traffic-not-permitted .

When I try to do something similar, I don't create two types of application-components, just one. I provide them with different inputs, based on whether it's for the actual App or for the TestApp . No need for TestAppComponent at all. Eg

open class App : Application(), HasAndroidInjector {

    lateinit var application: Application

    @Inject
    lateinit var androidInjector: DispatchingAndroidInjector<Any>

    override fun androidInjector(): AndroidInjector<Any> = androidInjector

    override fun onCreate() {
        super.onCreate()
        DaggerAppComponent.factory()
            .create(this, createRetrofitModule())
            .inject(this)
        this.application = this
    }

    protected fun createRetrofitModule() = RetrofitModule(BuildConfig.BASE_URL)
}

class TestApp : App() {
    override fun createRetrofitModule() = RetrofitModule("http://localhost:8080")
}


@Module
class RetrofitModule(private val baseUrl: String) {
    ...
    provide your Retrofit and OkHttpClients here and use the 'baseUrl'.
    ...
}

(not sure if this 'compiles' or not; I usually use the builder() pattern on a dagger-component, not the factory() pattern, but you get the idea).

The pattern here is to provide your app-component or its modules with inputs for the 'edge-of-the-world', stuff that needs to be configured differently based on the context in which the app would run (contexts such as build-flavors, app running on consumer device vs running in instrumentation mode, etc). Examples are BuildConfig values (such as base-urls for networking), interface-implementations to real or fake hardware, interfaces to 3rd party libs, etc.

How about a dagger module for your Test Class with a ContributeAndroidInjector in there and do Inject on a @Before method.

Your TestAppComponent :

@Component(modules = [AndroidInjectionModule::class, TestAppModule::class])
interface TestAppComponent {
    fun inject(app: TestApp)

    @Component.Builder
    interface Builder {
        @BindsInstance
        fun application(application: TestApp): Builder
        fun build(): TestAppComponent
    }
}

TestAppModule like:

@Module
interface TestAppModule {
    @ContributesAndroidInjector(modules = [Provider::class])
    fun activity(): MainActivity

    @Module
    object Provider {
        @Provides
        @JvmStatic
        fun provideString(): String = "This is test."

    }

    // Your other dependencies here
}

And @Before method of Test Class you must be do:

@Before
fun setUp() {
    val instrumentation = InstrumentationRegistry.getInstrumentation()
    val app = instrumentation.targetContext.applicationContext as TestApp

    DaggerTestAppComponent.builder().application(app).build().inject(app)

   // Some things other
}

An important thing, you must be have (on build.gradle module app ):

kaptAndroidTest "com.google.dagger:dagger-compiler:$version_dagger"
kaptAndroidTest "com.google.dagger:dagger-android-processor:$version"

Now, when you launch an Activity like MainActivity , Dagger will inject dependencies from your TestAppModule instead of AppModule before.

Moreover, If you want to @Inject to Test Class , you can add:

fun inject(testClass: TestClass) // On Your TestAppComponent

And then, you can call:

DaggerTestAppComponent.builder().application(app).build().inject(this) // This is on your TestClass

to Inject some dependencies to your TestClass .

Hope this can help you!!

I am presuming that you are trying to inject OkHttpClient:

@Inject
lateinit var okHttpClient: OkHttpClient

in your TestApp class, and it fails. In order to make it work, you will need to add an inject method in your TestAppComponent , to inject the overriden TestApp, so that it becomes:

@Singleton
@Component(
    modules = [
        AndroidInjectionModule::class,
        AppModule::class,
        TestRetrofitModule::class,
        AppFeaturesModule::class,
        RoomModule::class]
)
interface TestAppComponent : AppComponent {
    @Component.Factory
    interface Factory {
        fun create(@BindsInstance application: App): TestAppComponent
    }

    fun inject(testApp: TestApp)
}

The reason why this is required, is because Dagger is type based, and uses the type of each class to be injected/provided to determine how to generate the code at compile-time. In your case, when you try to inject the TestApp, dagger will inject its superclass (the App class), because it only know that it has to inject the App class. If you have a look at the AndroidInjector interface (that you use in your AppComponent), you will see that it is declared like:

public interface AndroidInjector<T> {
    void inject(T instance)
....
}

This means that it will generate a method:

fun inject(app App)

in the AppComponent. And this is why @Inject works in your App class, but does not work in your TestApp class, unless you explicitly provided it in the TestAppComponent.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM