Remplacer la liaison dans Guice

138

Je viens de commencer à jouer avec Guice, et un cas d'utilisation auquel je peux penser est que dans un test, je veux juste remplacer une seule liaison. Je pense que j'aimerais utiliser le reste des liaisons au niveau de la production pour m'assurer que tout est correctement configuré et éviter la duplication.

Alors imaginez que j'ai le module suivant

public class ProductionModule implements Module {
    public void configure(Binder binder) {
        binder.bind(InterfaceA.class).to(ConcreteA.class);
        binder.bind(InterfaceB.class).to(ConcreteB.class);
        binder.bind(InterfaceC.class).to(ConcreteC.class);
    }
}

Et dans mon test, je veux seulement remplacer InterfaceC, tout en gardant InterfaceA et InterfaceB intacts, donc je voudrais quelque chose comme:

Module testModule = new Module() {
    public void configure(Binder binder) {
        binder.bind(InterfaceC.class).to(MockC.class);
    }
};
Guice.createInjector(new ProductionModule(), testModule);

J'ai également essayé ce qui suit, sans succès:

Module testModule = new ProductionModule() {
    public void configure(Binder binder) {
        super.configure(binder);
        binder.bind(InterfaceC.class).to(MockC.class);
    }
};
Guice.createInjector(testModule);

Est-ce que quelqu'un sait s'il est possible de faire ce que je veux ou est-ce que j'aboie complètement le mauvais arbre?

--- Suivi: Il semblerait que je puisse réaliser ce que je veux si j'utilise la balise @ImplementedBy sur l'interface et que je fournis simplement une liaison dans le cas de test, ce qui fonctionne bien quand il y a un mappage 1-1 entre l'interface et la mise en œuvre.

De plus, après en avoir discuté avec un collègue, il semblerait que nous prenions la route pour remplacer un module entier et nous assurer que nos modules sont définis correctement. Cela semble cependant causer un problème lorsqu'une liaison est mal placée dans un module et doit être déplacée, ce qui peut éventuellement interrompre une charge de tests car les liaisons peuvent ne plus être disponibles pour être remplacées.

tddmonkey
la source
7
Comme la phrase «aboyer le mauvais arbre»: D
Boris Pavlović

Réponses:

149

Ce n'est peut-être pas la réponse que vous recherchez, mais si vous écrivez des tests unitaires, vous ne devriez probablement pas utiliser d'injecteur et plutôt injecter des objets factices ou faux à la main.

En revanche, si vous souhaitez vraiment remplacer une seule liaison, vous pouvez utiliser Modules.override(..):

public class ProductionModule implements Module {
    public void configure(Binder binder) {
        binder.bind(InterfaceA.class).to(ConcreteA.class);
        binder.bind(InterfaceB.class).to(ConcreteB.class);
        binder.bind(InterfaceC.class).to(ConcreteC.class);
    }
}
public class TestModule implements Module {
    public void configure(Binder binder) {
        binder.bind(InterfaceC.class).to(MockC.class);
    }
}
Guice.createInjector(Modules.override(new ProductionModule()).with(new TestModule()));

Voir les détails ici .

Mais comme le javadoc pour le Modules.overrides(..)recommande, vous devez concevoir vos modules de telle manière que vous n'ayez pas besoin de remplacer les liaisons. Dans l'exemple que vous avez donné, vous pouvez accomplir cela en déplaçant la liaison de InterfaceCvers un module distinct.

Albertb
la source
9
Merci Albert, cela me permet de faire ce que je veux. C'est dans une version de production pourtant tho! Et c'est pour les tests d'intégration, pas pour les tests unitaires, c'est pourquoi je veux m'assurer que tout le reste est construit correctement
tddmonkey
1
J'ai ajouté un exemple concret au code. Cela vous fait-il avancer?
albertb
1
Sauf erreur de ma part, ovverideperd le bon Stageen le faisant (ie DÉVELOPPEMENT est systématiquement utilisé).
pdeschen
4
Questions de taille. Lorsque votre graphe de dépendance augmente, le câblage manuel peut être assez pénible. De plus, lorsque le câblage change, vous devez mettre à jour manuellement tous vos emplacements de câblage manuel. Le remplacement vous permet de gérer cela automatiquement.
yoosiba
3
@pdeschen C'est un bug dans Guice 3 que j'ai corrigé pour Guice 4.
Tavian Barnes
9

Pourquoi ne pas utiliser l'héritage? Vous pouvez remplacer vos liaisons spécifiques dans overrideMemethod, laissant les implémentations partagées dans configuremethod.

public class DevModule implements Module {
    public void configure(Binder binder) {
        binder.bind(InterfaceA.class).to(TestDevImplA.class);
        overrideMe(binder);
    }

    protected void overrideMe(Binder binder){
        binder.bind(InterfaceC.class).to(ConcreteC.class);
    }
};

public class TestModule extends DevModule {
    @Override
    public void overrideMe(Binder binder) {
        binder.bind(InterfaceC.class).to(MockC.class);
    }
}

Et enfin créez votre injecteur de cette façon:

Guice.createInjector(new TestModule());
Mon Calamars
la source
3
Le @Overridene semble pas fonctionner. Surtout si c'est fait sur une méthode que @Providesquelque chose.
Sasanka Panguluri
4

Si vous ne souhaitez pas modifier votre module de production et si vous avez une structure de projet de type maven par défaut comme

src/test/java/...
src/main/java/...

Vous pouvez simplement créer une nouvelle classe ConcreteCdans votre répertoire de test en utilisant le même package que pour votre classe d'origine. Guice se liera alors InterfaceCà ConcreteCpartir de votre répertoire de test tandis que toutes les autres interfaces seront liées à vos classes de production.

Jan Gassen
la source
2

Vous souhaitez utiliser Juckito où vous pouvez déclarer votre configuration personnalisée pour chaque classe de test.

@RunWith(JukitoRunner.class)
class LogicTest {
    public static class Module extends JukitoModule {

        @Override
        protected void configureTest() {
            bind(InterfaceC.class).to(MockC.class);
        }
    }

    @Inject
    private InterfaceC logic;

    @Test
    public testLogicUsingMock() {
        logic.foo();
    }
}
esukram
la source
1

Dans une configuration différente, nous avons plus d'une activité définie dans des modules séparés. L'activité qui est injectée se trouve dans un module de bibliothèque Android, avec sa propre définition de module RoboGuice dans le fichier AndroidManifest.xml.

La configuration ressemble à ceci. Dans le module Bibliothèque, il y a ces définitions:

AndroidManifest.xml:

<application android:allowBackup="true">
    <activity android:name="com.example.SomeActivity/>
    <meta-data
        android:name="roboguice.modules"
        android:value="com.example.MainModule" />
</application>

Ensuite, nous avons un type en cours d'injection:

interface Foo { }

Une implémentation par défaut de Foo:

class FooThing implements Foo { }

MainModule configure l'implémentation FooThing pour Foo:

public class MainModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(Foo.class).to(FooThing.class);
    }
}

Et enfin, une activité qui consomme Foo:

public class SomeActivity extends RoboActivity {
    @Inject
    private Foo foo;
}

Dans le module d'application Android, nous aimerions utiliser SomeActivitymais, à des fins de test, injectons le nôtre Foo.

public class SomeOtherActivity extends Activity {
    @Override
    protected void onResume() {
        super.onResume();

        Intent intent = new Intent(this, SomeActivity.class);
        startActivity(intent);
    }
}

On pourrait argumenter pour exposer la gestion du module à l'application cliente, cependant, nous devons principalement masquer les composants en cours d'injection car le module de bibliothèque est un SDK et l'exposition de pièces a des implications plus importantes.

(Rappelez-vous, c'est pour les tests, donc nous connaissons les composants internes de SomeActivity, et savons qu'il consomme un (package visible) Foo).

La façon dont j'ai trouvé que cela fonctionne a du sens; utilisez le remplacement suggéré pour les tests :

public class SomeOtherActivity extends Activity {
    private class OverrideModule
            extends AbstractModule {

        @Override
        protected void configure() {
            bind(Foo.class).to(OtherFooThing.class);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        RoboGuice.overrideApplicationInjector(
                getApplication(),
                RoboGuice.newDefaultRoboModule(getApplication()),
                Modules
                        .override(new MainModule())
                        .with(new OverrideModule()));
    }

    @Override
    protected void onResume() {
        super.onResume();

        Intent intent = new Intent(this, SomeActivity.class);
        startActivity(intent);
    }
}

Maintenant, au SomeActivitydémarrage, il obtiendra OtherFooThingpour son Fooinstance injectée .

C'est une situation très spécifique où, dans notre cas, OtherFooThing a été utilisé en interne pour enregistrer des situations de test, tandis que FooThing a été utilisé, par défaut, pour toutes les autres utilisations.

Gardez à l' esprit, nous sommes utilisons #newDefaultRoboModuledans nos tests unitaires, et il fonctionne parfaitement.

Dave T.
la source