web-dev-qa-db-fra.com

(DAGGER-Android) Impossible d'utiliser @Inject sur un test Espresso et ne peut pas utiliser mockWebServer

J'essaie de créer des tests Espresso et en utilisant un mockWebServer, le problème est que lorsque j'essaie de créer mon mockWebServer, il appelle le vrai appel d'API et je veux l'intercepter et me moquer de la réponse.

Mon organisation de poignard est:

Mon appli

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

Puis 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
    }
}

Ensuite, j'ai créé cette TestApp

class TestApp : App() {

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

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

Et c'est mon 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
    }
}

Remarque: Ici, j'ai créé un nouveau module, appelé TestRetrofitModule où BASE_URL est " http: // localhost: 808 ", je ne sais pas si j'ai besoin d'autre chose.

J'ai également créé le TestRunner

class TestRunner : AndroidJUnitRunner() {

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

}

Et mettez-le sur le testInstrumentationRunner

Problème 1

Je ne peux pas utiliser

@Inject
lateinit var okHttpClient: OkHttpClient

car il dit qu'il n'est pas initialisé.

Problème 2 (résolu grâce à Skizo)

Mon mockWebServer ne distribue pas les réponses même s'il ne pointe pas le vrai appel api, pointe celui que j'ai mis vers le TestRetrofitModule, le fait est que je dois lier ce mockWebServer et Retrofit.

7
StuartDTO

Lorsque j'essaie de faire quelque chose de similaire, je ne crée pas deux types de composants d'application, un seul. Je leur fournit différentes entrées, selon que ce soit pour le App ou pour le TestApp. Pas besoin du tout de TestAppComponent. Par exemple.

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'.
    ...
}

(je ne sais pas si cela 'compile' ou non; j'utilise généralement le motif builder() sur un composant de poignard, pas le motif factory(), mais vous voyez l'idée).

Le modèle ici consiste à fournir à votre composant d'application ou à ses modules des entrées pour le 'Edge-of-the-world', des éléments qui doivent être configurés différemment en fonction du contexte dans lequel l'application s'exécuterait (contextes tels que build- saveurs, application exécutée sur un appareil grand public ou exécutée en mode instrumentation, etc.). Des exemples sont les valeurs BuildConfig (telles que les URL de base pour la mise en réseau), les implémentations d'interface vers du matériel réel ou faux, les interfaces vers des bibliothèques tierces, etc.

1

Que diriez-vous a dagger module pour votre Test Class avec un ContributeAndroidInjector dedans et faire Inject sur un @Before méthode.

Votre 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 comme:

@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
}

Et @Before méthode de Test Class vous devez faire:

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

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

   // Some things other
}

Une chose importante, vous devez avoir (sur build.gradle module app):

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

Maintenant, lorsque vous lancez un Activity comme MainActivity, Dagger injectera dependencies depuis votre TestAppModule au lieu de AppModule avant.

De plus, si vous voulez @Inject à Test Class, vous pouvez ajouter:

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

Et puis, vous pouvez appeler:

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

pour Injecter des dépendances dans votre TestClass.

J'espère que cela peut vous aider !!

1
leo

Je suppose que vous essayez d'injecter OkHttpClient:

@Inject
lateinit var okHttpClient: OkHttpClient

dans votre classe TestApp, et cela échoue. Afin de le faire fonctionner, vous devrez ajouter une méthode d'injection dans votre TestAppComponent, pour injecter le TestApp remplacé, de sorte qu'il devienne:

@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)
}

La raison pour laquelle cela est nécessaire est que Dagger est basé sur le type et utilise le type de chaque classe à injecter/fournir pour déterminer comment générer le code au moment de la compilation. Dans votre cas, lorsque vous essayez d'injecter le TestApp, le poignard injectera sa superclasse (la classe App), car il sait seulement qu'il doit injecter la classe App. Si vous regardez l'interface AndroidInjector (que vous utilisez dans votre AppComponent), vous verrez qu'elle est déclarée comme:

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

Cela signifie qu'il générera une méthode:

fun inject(app App)

dans AppComponent. Et c'est pourquoi @Inject fonctionne dans votre classe App, mais ne fonctionne pas dans votre classe TestApp, sauf si vous l'avez explicitement fourni dans TestAppComponent.

0
gmetal