7

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.

StuartDTO
  • 783
  • 7
  • 26
  • 72

5 Answers5

3

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
wasyl
  • 3,421
  • 3
  • 27
  • 36
  • Yes, that's correct, but the thing is the Dispatcher is not working, how do I link the mockWebServer to the fake call? – StuartDTO May 20 '20 at 16:00
2

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.

Skizo-ozᴉʞS ツ
  • 19,464
  • 18
  • 81
  • 148
1

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. E.g.

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.

Streets Of Boston
  • 12,576
  • 2
  • 25
  • 28
  • There are two problems there, one that I can use localhost to be the same as mockwebserver and is working, the problem I guess that the ports are differents and that's why is not working, could be? – StuartDTO May 21 '20 at 08:09
  • 1
    StuartDTO: Your second problem may have been answered by Skizo-ozᴉʞS earlier. – Streets Of Boston May 21 '20 at 13:21
  • True, it did. Thanks. Now the problem is that I can not use Inject to get anything from TestAppComponent... – StuartDTO May 21 '20 at 13:31
1

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!!

dinhlam
  • 708
  • 3
  • 14
  • Looks what I need, could you post like an example of what I have to do so I can try= – StuartDTO May 26 '20 at 13:10
  • I supposed that, but it means that I have to add as much inject as test I have?... – StuartDTO May 27 '20 at 08:50
  • No, when you create TestModule you can `inheritance` from `your main module` and do `override` on necessary dependencies... – dinhlam May 27 '20 at 08:56
  • @StuartDTO Maybe you can check an example at: https://github.com/dinhlamvn/uitestDagger. I have added an TestClass and do some inject dependencies with Dagger on there. – dinhlam May 27 '20 at 17:46
  • Now you have this `fun inject(test: ExampleInstrumentedTest)` but then if I want to inject it in another test I do not need it because I have this `BaseTest`? – StuartDTO May 28 '20 at 06:50
  • Maybe that's not correct, because Dagger just `inject` to specify `Class`, that's reason you will have many `inject` method on `Component` or have `ContributeAndroidInjector` method on `Module`. – dinhlam May 28 '20 at 07:10
  • @StuartDTO you can get more detail in: https://stackoverflow.com/questions/29312911/can-i-just-inject-super-class-when-use-dagger2-for-dependency-injection – dinhlam May 28 '20 at 07:22
0

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.

gmetal
  • 2,908
  • 2
  • 11
  • 16
  • So I have to inject it on every test? I mean in `@Before` do I have to inject it? How do I do inject it? I'm trying to do it with dagger-android without the field-injection? – StuartDTO May 26 '20 at 13:11
  • @StuartDTO it is not quite clear where you want to inject the OkHttpClient, or if you need to inject the OkHttpClient. Since you have created a TestRetrofitModule, you do not need to inject the OkHttpClient anywhere. All the necessary wiring should be handled in your TestRetrofitModule class. My answer assumes that you tried to inject variables in the TestApp class, which is not automatically set as to be injected (because it does not have an inject method call in the TestApplicationComponent). All classes which have variables injected, should have an inject method call in a Component. – gmetal May 26 '20 at 13:54
  • But in case I need to inject something like Room or some preferences, how do I get them if I can not use @inject? Also, I'm creating the mockWebServer in the test and I thought it could be possible to have it as a provides but is giving me NOMTException – StuartDTO May 27 '20 at 08:52
  • You can inject anything you like in your test classes. But in to achieve this, you should include an `inject()` function in your TestAppComponent and explicitly inject your test class. Unless you add an inject function in your TestAppComponent, and explicitly inject your Test case in your @Before method, the dependencies will not be supplied to your Test class. – gmetal May 27 '20 at 09:27