Comment remplacer un module / une dépendance dans un test unitaire avec Dagger 2.0?

J’ai une activité Android simple avec une seule dépendance. J’injecte la dépendance dans le onCreate l’activité comme ceci:

 Dagger_HelloComponent.builder() .helloModule(new HelloModule(this)) .build() .initialize(this); 

Dans mon ActivityUnitTestCase je veux remplacer la dépendance par un simulacre de Mockito. Je suppose que je dois utiliser un module spécifique au test qui fournit le simulacre, mais je ne peux pas comprendre comment append ce module au graphe d’object.

Dans Dagger 1.x, ceci est apparemment fait avec quelque chose comme ceci :

 @Before public void setUp() { ObjectGraph.create(new TestModule()).inject(this); } 

Quelle est l’équivalent de Dagger 2.0?

Vous pouvez voir mon projet et son test unitaire ici sur GitHub .

Il s’agit probablement d’une solution de contournement qui prend en charge le remplacement des modules de test, mais elle permet de remplacer les modules de production par un test. Les extraits de code ci-dessous montrent des cas simples lorsque vous avez un seul composant et un seul module, mais cela devrait fonctionner pour tous les scénarios. Cela nécessite beaucoup de répétition de code et de code, alors soyez conscient de cela. Je suis sûr qu’il y aura un meilleur moyen d’y parvenir à l’avenir.

J’ai également créé un projet avec des exemples pour Espresso et Robolecsortingc . Cette réponse est basée sur le code contenu dans le projet.

La solution nécessite deux choses:

  • fournir un setter supplémentaire pour @Component
  • composant de test doit étendre le composant de production

Supposons que l’ Application simple comme ci-dessous:

 public class App extends Application { private AppComponent mAppComponent; @Override public void onCreate() { super.onCreate(); mAppComponent = DaggerApp_AppComponent.create(); } public AppComponent component() { return mAppComponent; } @Singleton @Component(modules = SsortingngHolderModule.class) public interface AppComponent { void inject(MainActivity activity); } @Module public static class SsortingngHolderModule { @Provides SsortingngHolder provideSsortingng() { return new SsortingngHolder("Release ssortingng"); } } } 

Nous devons append une méthode supplémentaire à la classe App . Cela nous permet de remplacer le composant de production.

 /** * Visible only for testing purposes. */ // @VisibleForTesting public void setTestComponent(AppComponent appComponent) { mAppComponent = appComponent; } 

Comme vous pouvez le voir, l’object SsortingngHolder contient la valeur “Release ssortingng”. Cet object est injecté dans le MainActivity .

 public class MainActivity extends ActionBarActivity { @Inject SsortingngHolder mSsortingngHolder; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((App) getApplication()).component().inject(this); } } 

Dans nos tests, nous voulons fournir SsortingngHolder avec “Test ssortingng”. Nous devons définir le composant de test dans la classe App avant la MainActivity – car SsortingngHolder est injecté dans le rappel onCreate .

Dans Dagger v2.0.0, les composants peuvent étendre d’autres interfaces. Nous pouvons tirer parti de cela pour créer notre TestAppComponent qui étend AppComponent .

 @Component(modules = TestSsortingngHolderModule.class) interface TestAppComponent extends AppComponent { } 

Nous pouvons maintenant définir nos modules de test, par exemple TestSsortingngHolderModule . La dernière étape consiste à définir le composant de test en utilisant la méthode setter précédemment ajoutée dans la classe App . Il est important de le faire avant la création de l’activité.

 ((App) application).setTestComponent(mTestAppComponent); 

Expresso

Pour Espresso, j’ai créé ActivityTestRule personnalisé qui permet d’échanger le composant avant la création de l’activité. Vous pouvez trouver le code pour DaggerActivityTestRule ici .

Exemple de test avec Espresso:

 @RunWith(AndroidJUnit4.class) @LargeTest public class MainActivityEspressoTest { public static final Ssortingng TEST_STRING = "Test ssortingng"; private TestAppComponent mTestAppComponent; @Rule public ActivityTestRule mActivityRule = new DaggerActivityTestRule<>(MainActivity.class, new OnBeforeActivityLaunchedListener() { @Override public void beforeActivityLaunched(@NonNull Application application, @NonNull MainActivity activity) { mTestAppComponent = DaggerMainActivityEspressoTest_TestAppComponent.create(); ((App) application).setTestComponent(mTestAppComponent); } }); @Component(modules = TestSsortingngHolderModule.class) interface TestAppComponent extends AppComponent { } @Module static class TestSsortingngHolderModule { @Provides SsortingngHolder provideSsortingng() { return new SsortingngHolder(TEST_STRING); } } @Test public void checkSomething() { // given ... // when onView(...) // then onView(...) .check(...); } } 

Robolecsortingque

C’est beaucoup plus facile avec Robolecsortingc grâce à l’application RuntimeEnvironment.application .

Exemple de test avec Robolecsortingc:

 @RunWith(RobolecsortingcGradleTestRunner.class) @Config(emulateSdk = 21, reportSdk = 21, constants = BuildConfig.class) public class MainActivityRobolecsortingcTest { public static final Ssortingng TEST_STRING = "Test ssortingng"; @Before public void setTestComponent() { AppComponent appComponent = DaggerMainActivityRobolecsortingcTest_TestAppComponent.create(); ((App) RuntimeEnvironment.application).setTestComponent(appComponent); } @Component(modules = TestSsortingngHolderModule.class) interface TestAppComponent extends AppComponent { } @Module static class TestSsortingngHolderModule { @Provides SsortingngHolder provideSsortingng() { return new SsortingngHolder(TEST_STRING); } } @Test public void checkSomething() { // given MainActivity mainActivity = Robolecsortingc.setupActivity(MainActivity.class); // when ... // then assertThat(...) } } 

Comme le dit @EpicPandaForce à juste titre, vous ne pouvez pas étendre les modules. Cependant, j’ai trouvé une solution sournoise à ce sujet, ce qui, à mon avis, évite une grande partie du passe-partout dont souffrent les autres exemples.

L’astuce pour «étendre» un module consiste à créer une maquette partielle et à simuler les méthodes de fournisseur que vous souhaitez remplacer.

En utilisant Mockito :

 MyModule module = Mockito.spy(new MyModule()); Mockito.doReturn("mocked ssortingng").when(module).provideSsortingng(); MyComponent component = DaggerMyComponent.builder() .myModule(module) .build(); app.setComponent(component); 

J’ai créé cet aperçu ici pour montrer un exemple complet.

MODIFIER

Il se trouve que vous pouvez le faire même sans un simulacre partiel, comme ceci:

 MyComponent component = DaggerMyComponent.builder() .myModule(new MyModule() { @Override public Ssortingng provideSsortingng() { return "mocked ssortingng"; } }) .build(); app.setComponent(component); 

La solution de contournement proposée par @tomrozb est très bonne et me met sur la bonne voie, mais le problème avec cela était qu’elle exposait une méthode setTestComponent() dans la classe Application PRODUCTION. J’ai pu obtenir un résultat légèrement différent, de sorte que mon application de production ne doit rien savoir de mon environnement de test.

TL; DR – Étendez votre classe d’application avec une application de test qui utilise votre composant de test et votre module. Créez ensuite un programme de test personnalisé qui s’exécute sur l’application de test au lieu de votre application de production.


EDIT: Cette méthode ne fonctionne que pour les dépendances globales (généralement marquées par @Singleton ). Si votre application a des composants de scope différente (par exemple, par activité), vous devrez soit créer des sous-classes pour chaque domaine, soit utiliser la réponse originale de @tomrozb. Merci à @tomrozb de l’avoir signalé!


Cet exemple utilise le programme de test AndroidJUnitRunner , mais cela pourrait probablement être adapté à Robolecsortingc et à d’autres.

Tout d’abord, mon application de production. Cela ressemble à ceci:

 public class MyApp extends Application { protected MyComponent component; public void setComponent() { component = DaggerMyComponent.builder() .myModule(new MyModule()) .build(); component.inject(this); } public MyComponent getComponent() { return component; } @Override public void onCreate() { super.onCreate(); setComponent(); } } 

De cette façon, mes activités et les autres classes qui utilisent @Inject doivent simplement appeler quelque chose comme getApp().getComponent().inject(this); s’injecter dans le graphe de dépendance.

Pour être complet, voici mon composant:

 @Singleton @Component(modules = {MyModule.class}) public interface MyComponent { void inject(MyApp app); // other injects and getters } 

Et mon module:

 @Module public class MyModule { // EDIT: This solution only works for global dependencies @Provides @Singleton public MyClass provideMyClass() { ... } // ... other providers } 

Pour l’environnement de test, étendez votre composant de test à partir de votre composant de production. C’est la même chose que dans la réponse de @tomrozb.

 @Singleton @Component(modules = {MyTestModule.class}) public interface MyTestComponent extends MyComponent { // more component methods if necessary } 

Et le module de test peut être ce que vous voulez. Je suppose que vous manipulerez vos moqueries et autres trucs ici (j’utilise Mockito).

 @Module public class MyTestModule { // EDIT: This solution only works for global dependencies @Provides @Singleton public MyClass provideMyClass() { ... } // Make sure to implement all the same methods here that are in MyModule, // even though it's not an override. } 

Alors maintenant, la partie délicate. Créez une classe d’application de test qui s’étend de votre classe d’application de production et remplacez la méthode setComponent() pour définir le composant de test avec le module de test. Notez que cela ne peut fonctionner que si MyTestComponent est un descendant de MyComponent .

 public class MyTestApp extends MyApp { // Make sure to call this method during setup of your tests! @Override public void setComponent() { component = DaggerMyTestComponent.builder() .myTestModule(new MyTestModule()) .build(); component.inject(this) } } 

Assurez-vous d’appeler setComponent() sur l’application avant de commencer vos tests pour vous assurer que le graphique est correctement configuré. Quelque chose comme ça:

 @Before public void setUp() { MyTestApp app = (MyTestApp) getInstrumentation().getTargetContext().getApplicationContext(); app.setComponent() ((MyTestComponent) app.getComponent()).inject(this) } 

Enfin, la dernière pièce manquante consiste à remplacer votre TestRunner par un programme de test personnalisé. Dans mon projet, j’utilisais AndroidJUnitRunner mais il semble que vous puissiez faire la même chose avec Robolecsortingc .

 public class TestRunner extends AndroidJUnitRunner { @Override public Application newApplication(@NonNull ClassLoader cl, Ssortingng className, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException { return super.newApplication(cl, MyTestApp.class.getName(), context); } } 

Vous devrez également mettre à jour votre testInstrumentationRunner, comme ceci:

 testInstrumentationRunner "com.mypackage.TestRunner" 

Et si vous utilisez Android Studio, vous devrez également cliquer sur Modifier la configuration dans le menu Exécuter et entrer le nom de votre utilisateur sous “Runner d’instrumentation spécifique”.

Et c’est tout! Espérons que cette information aide quelqu’un 🙂

Il semble que j’ai trouvé une autre voie et que cela fonctionne jusqu’à présent.

Tout d’abord, une interface de composant qui n’est pas un composant lui-même:

MyComponent.java

 interface MyComponent { Foo provideFoo(); } 

Ensuite, nous avons deux modules différents: un réel et un test.

MyModule.java

 @Module class MyModule { @Provides public Foo getFoo() { return new Foo(); } } 

TestModule.java

 @Module class TestModule { private Foo foo; public void setFoo(Foo foo) { this.foo = foo; } @Provides public Foo getFoo() { return foo; } } 

Et nous avons deux composants pour utiliser ces deux modules:

MyRealComponent.java

 @Component(modules=MyModule.class) interface MyRealComponent extends MyComponent { Foo provideFoo(); // without this dagger will not do its magic } 

MyTestComponent.java

 @Component(modules=TestModule.class) interface MyTestComponent extends MyComponent { Foo provideFoo(); } 

Dans l’application, nous faisons ceci:

 MyComponent component = DaggerMyRealComponent.create(); <...> Foo foo = component.getFoo(); 

Dans le code de test, nous utilisons:

 TestModule testModule = new TestModule(); testModule.setFoo(someMockFoo); MyComponent component = DaggerMyTestComponent.builder() .testModule(testModule).build(); <...> Foo foo = component.getFoo(); // will return someMockFoo 

Le problème est que nous devons copier toutes les méthodes de MyModule dans TestModule, mais cela peut être fait en ayant MyModule dans TestModule et en utilisant les méthodes de MyModule, sauf si elles sont directement définies de l’extérieur. Comme ça:

TestModule.java

 @Module class TestModule { MyModule myModule = new MyModule(); private Foo foo = myModule.getFoo(); public void setFoo(Foo foo) { this.foo = foo; } @Provides public Foo getFoo() { return foo; } } 

Cette réponse est obsolète. LIRE CI-DESSOUS DANS EDIT.

De manière décevante, vous ne pouvez pas étendre à partir d’un module , ou vous obtiendrez l’erreur de compilation suivante:

 Error:(24, 21) error: @Provides methods may not override another method. Overrides: Provides retrofit.Endpoint hu.mycompany.injection.modules.application.domain.networking.EndpointModule.mySe‌​rverEndpoint() 

Cela signifie que vous ne pouvez pas simplement étendre un “module simulé” et remplacer votre module d’origine. Non, ce n’est pas si facile. Et étant donné que vous concevez vos composants de telle manière qu’il lie directement les modules par classe, vous ne pouvez pas simplement créer un “TestComponent”, car cela signifie que vous devrez tout réinventer et que vous aurez faire un composant pour chaque variation! Clairement, ce n’est pas une option.

Donc, à plus petite échelle, j’ai fini par créer un “fournisseur” que je donne au module, qui détermine si je sélectionne le modèle ou le type de production.

 public interface EndpointProvider { Endpoint serverEndpoint(); } public class ProdEndpointProvider implements EndpointProvider { @Override public Endpoint serverEndpoint() { return new ServerEndpoint(); } } public class TestEndpointProvider implements EndpointProvider { @Override public Endpoint serverEndpoint() { return new TestServerEndpoint(); } } @Module public class EndpointModule { private Endpoint serverEndpoint; private EndpointProvider endpointProvider; public EndpointModule(EndpointProvider endpointProvider) { this.endpointProvider = endpointProvider; } @Named("server") @Provides public Endpoint serverEndpoint() { return endpointProvider.serverEndpoint(); } } 

EDIT: Apparemment, comme le message d’erreur le dit, vous ne pouvez pas remplacer une autre méthode en utilisant une méthode annotée @Provides , mais cela ne signifie pas que vous ne pouvez pas remplacer une méthode annotée @Provides 🙁

Toute cette magie était pour rien! Vous pouvez simplement étendre un module sans mettre @Provides sur la méthode et cela fonctionne … Reportez-vous à la réponse de @vaughandroid.

Pouvez-vous vérifier ma solution, j’ai inclus l’exemple de sous-composant: https://github.com/nongdenchet/android-mvvm-with-tests . Merci @vaughandroid, j’ai emprunté vos méthodes primordiales. Voici le point principal:

  1. Je crée une classe pour créer un sous-composant. Mon application personnalisée contiendra également une instance de cette classe:

     // The builder class public class ComponentBuilder { private AppComponent appComponent; public ComponentBuilder(AppComponent appComponent) { this.appComponent = appComponent; } public PlacesComponent placesComponent() { return appComponent.plus(new PlacesModule()); } public PurchaseComponent purchaseComponent() { return appComponent.plus(new PurchaseModule()); } } // My custom application class public class MyApplication extends Application { protected AppComponent mAppComponent; protected ComponentBuilder mComponentBuilder; @Override public void onCreate() { super.onCreate(); // Create app component mAppComponent = DaggerAppComponent.builder() .appModule(new AppModule()) .build(); // Create component builder mComponentBuilder = new ComponentBuilder(mAppComponent); } public AppComponent component() { return mAppComponent; } public ComponentBuilder builder() { return mComponentBuilder; } } // Sample using builder class: public class PurchaseActivity extends BaseActivity { ... @Override protected void onCreate(Bundle savedInstanceState) { ... // Setup dependency ((MyApplication) getApplication()) .builder() .purchaseComponent() .inject(this); ... } } 
  2. J’ai un TestApplication personnalisé qui étend la classe MyApplication ci-dessus. Cette classe contient deux méthodes pour remplacer le composant racine et le générateur:

     public class TestApplication extends MyApplication { public void setComponent(AppComponent appComponent) { this.mAppComponent = appComponent; } public void setComponentBuilder(ComponentBuilder componentBuilder) { this.mComponentBuilder = componentBuilder; } } 
  3. Enfin, je vais essayer de simuler ou de supprimer la dépendance du module et du générateur pour fournir de fausses dépendances à l’activité:

     @MediumTest @RunWith(AndroidJUnit4.class) public class PurchaseActivityTest { @Rule public ActivityTestRule activityTestRule = new ActivityTestRule<>(PurchaseActivity.class, true, false); @Before public void setUp() throws Exception { PurchaseModule stubModule = new PurchaseModule() { @Provides @ViewScope public IPurchaseViewModel providePurchaseViewModel(IPurchaseApi purchaseApi) { return new StubPurchaseViewModel(); } }; // Setup test component AppComponent component = ApplicationUtils.application().component(); ApplicationUtils.application().setComponentBuilder(new ComponentBuilder(component) { @Override public PurchaseComponent purchaseComponent() { return component.plus(stubModule); } }); // Run the activity activityTestRule.launchActivity(new Intent()); } 

Avec Dagger2, vous pouvez transmettre un module spécifique (le TestModule) à un composant à l’aide de l’API générée.

 ApplicationComponent appComponent = Dagger_ApplicationComponent.builder() .helloModule(new TestModule()) .build(); 

Veuillez noter que Dagger_ApplicationComponent est une classe générée avec la nouvelle annotation @Component.