Reputation: 1051
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
I can not use
@Inject
lateinit var okHttpClient: OkHttpClient
because it says that it's not initialised.
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.
Upvotes: 7
Views: 725
Reputation: 765
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!!
Upvotes: 1
Reputation: 2988
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.
Upvotes: 0
Reputation: 20646
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.
Upvotes: 2
Reputation: 12596
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.
Upvotes: 1
Reputation: 3506
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
Upvotes: 3