Überschreiben der Bindung in Guice

138

Ich habe gerade angefangen, mit Guice zu spielen, und ein Anwendungsfall, den ich mir vorstellen kann, ist, dass ich in einem Test nur eine einzelne Bindung überschreiben möchte. Ich denke, ich möchte den Rest der Bindungen auf Produktionsebene verwenden, um sicherzustellen, dass alles korrekt eingerichtet ist und um Doppelarbeit zu vermeiden.

Stellen Sie sich vor, ich habe das folgende Modul

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

Und in meinem Test möchte ich nur InterfaceC überschreiben, während InterfaceA und InterfaceB im Takt bleiben, also möchte ich etwas wie:

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

Ich habe auch folgendes versucht, ohne Glück:

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

Weiß jemand, ob es möglich ist, das zu tun, was ich will, oder belle ich den falschen Baum komplett an?

--- Follow-up: Es scheint, dass ich erreichen kann, was ich will, wenn ich das @ ImplementedBy-Tag auf der Schnittstelle verwende und dann einfach eine Bindung im Testfall bereitstelle, was gut funktioniert, wenn eine 1-1-Zuordnung zwischen besteht die Schnittstelle und Implementierung.

Nachdem wir dies mit einem Kollegen besprochen haben, scheinen wir den Weg zu beschreiten, ein ganzes Modul zu überschreiben und sicherzustellen, dass unsere Module korrekt definiert sind. Dies scheint jedoch ein Problem zu verursachen, wenn eine Bindung in einem Modul falsch platziert ist und verschoben werden muss, wodurch möglicherweise eine Reihe von Tests unterbrochen werden, da Bindungen möglicherweise nicht mehr zum Überschreiben verfügbar sind.

tddmonkey
quelle
7
Wie der Satz "den falschen Baum bellen": D
Boris Pavlović

Antworten:

149

Dies ist möglicherweise nicht die Antwort, nach der Sie suchen, aber wenn Sie Komponententests schreiben, sollten Sie wahrscheinlich keinen Injektor verwenden und stattdessen Schein- oder Fälschungsobjekte von Hand injizieren.

Wenn Sie jedoch wirklich eine einzelne Bindung ersetzen möchten, können Sie Folgendes verwenden 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()));

Details finden Sie hier .

Aber wie der Javadoc für Modules.overrides(..)empfiehlt, sollten Sie Ihre Module so gestalten, dass Sie keine Bindungen überschreiben müssen. In dem von Ihnen angegebenen Beispiel können Sie dies erreichen, indem Sie die Bindung von InterfaceCin ein separates Modul verschieben.

albertb
quelle
9
Danke Albert, das bringt mich dazu, das zu tun, was ich will. Das ist noch in einer Produktionsversion! Und dies ist für Integrationstests, nicht für Unit-Tests, weshalb ich sicherstellen möchte, dass alles andere korrekt erstellt wird
tddmonkey
1
Ich habe dem Code ein konkretes Beispiel hinzugefügt. Kommt es dir weiter?
Albertb
1
Wenn ich mich nicht irre, ovverideverliert ich dabei das Richtige Stage(dh ENTWICKLUNG wird systematisch verwendet).
Pdeschen
4
Die Größe ist wichtig. Wenn Ihr Abhängigkeitsdiagramm wächst, kann die Verkabelung von Hand ziemlich schmerzhaft sein. Auch wenn sich die Verkabelung ändert, müssen Sie alle Ihre manuellen Verkabelungsstellen manuell aktualisieren. Durch Überschreiben können Sie dies automatisch erledigen.
Yoosiba
3
@pdeschen Das ist ein Fehler in Guice 3, den ich für Guice 4
behoben habe.
9

Warum nicht die Vererbung verwenden? Sie können Ihre spezifischen Bindungen in der overrideMeMethode überschreiben und gemeinsam genutzte Implementierungen in der configureMethode belassen.

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

Und schließlich erstellen Sie Ihren Injektor folgendermaßen:

Guice.createInjector(new TestModule());
Mon Calamari
quelle
3
Das @Overridescheint nicht zu funktionieren. Vor allem, wenn es mit einer Methode gemacht wird, dass @Providesetwas.
Sasanka Panguluri
4

Wenn Sie Ihr Produktionsmodul nicht ändern möchten und wenn Sie eine Standard-Maven-ähnliche Projektstruktur wie haben

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

Sie können einfach eine neue Klasse ConcreteCin Ihrem Testverzeichnis erstellen, indem Sie dasselbe Paket wie für Ihre ursprüngliche Klasse verwenden. Guice wird dann binden , InterfaceCum ConcreteCvon Ihrem Testverzeichnis , während alle anderen Schnittstellen an Ihre Produktion Klassen gebunden.

Jan Gassen
quelle
2

Sie möchten Juckito verwenden, wo Sie Ihre benutzerdefinierte Konfiguration für jede Testklasse deklarieren können.

@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
quelle
1

In einem anderen Setup haben wir mehr als eine Aktivität in separaten Modulen definiert. Die Aktivität, in die eingefügt wird, befindet sich in einem Android-Bibliotheksmodul mit einer eigenen RoboGuice-Moduldefinition in der Datei AndroidManifest.xml.

Das Setup sieht so aus. Im Bibliotheksmodul gibt es folgende Definitionen:

AndroidManifest.xml:

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

Dann wird ein Typ injiziert:

interface Foo { }

Einige Standardimplementierungen von Foo:

class FooThing implements Foo { }

MainModule konfiguriert die FooThing-Implementierung für Foo:

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

Und schließlich eine Aktivität, die Foo verbraucht:

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

Im konsumierenden Android-Anwendungsmodul möchten wir verwenden, SomeActivityaber zu Testzwecken unser eigenes injizieren Foo.

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

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

Man könnte argumentieren, die Modulbehandlung für die Clientanwendung verfügbar zu machen. Wir müssen jedoch die injizierten Komponenten größtenteils ausblenden, da das Bibliotheksmodul ein SDK ist und das Anzeigen von Teilen größere Auswirkungen hat.

(Denken Sie daran, dies dient zum Testen, damit wir die Interna von SomeActivity kennen und wissen, dass es ein (Paket sichtbar) Foo verbraucht.)

Die Art und Weise, wie ich fand, dass das funktioniert, macht Sinn; Verwenden Sie zum Testen die vorgeschlagene Überschreibung :

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

Wenn SomeActivityes jetzt gestartet wird, wird es OtherFooThingfür seine injizierte FooInstanz abgerufen .

Es ist eine sehr spezielle Situation, in der in unserem Fall OtherFooThing intern zum Aufzeichnen von Testsituationen verwendet wurde, während FooThing standardmäßig für alle anderen Zwecke verwendet wurde.

Denken Sie daran, wir sind mit #newDefaultRoboModuleunseren Unit - Tests, und es funktioniert einwandfrei.

Dave T.
quelle