Zastępowanie powiązania w Guice

138

Właśnie zacząłem grać z Guice i przypadkiem użycia, który przychodzi mi do głowy, jest to, że w teście chcę zastąpić pojedyncze wiązanie. Myślę, że chciałbym użyć pozostałych powiązań na poziomie produkcyjnym, aby upewnić się, że wszystko jest poprawnie skonfigurowane i uniknąć duplikacji.

Więc wyobraź sobie, że mam następujący moduł

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

W moim teście chcę tylko przesłonić InterfaceC, zachowując jednocześnie InterfaceA i InterfaceB, więc chciałbym coś takiego:

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

Próbowałem również następujących rzeczy, bez powodzenia:

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

Czy ktoś wie, czy można robić to, co chcę, czy też całkowicie szczekam na niewłaściwe drzewo?

--- Kontynuacja: wydaje się, że mogę osiągnąć to, co chcę, jeśli użyję tagu @ImplementedBy w interfejsie, a następnie po prostu dostarczę powiązanie w przypadku testowym, co działa dobrze, gdy istnieje mapowanie 1-1 między interfejs i implementacja.

Ponadto po omówieniu tego ze współpracownikiem wydaje się, że wybralibyśmy drogę zastąpienia całego modułu i upewnienia się, że nasze moduły zostały poprawnie zdefiniowane. Wydaje się, że może to jednak powodować problem, gdy powiązanie jest niewłaściwie umieszczone w module i musi zostać przeniesione, co może spowodować zerwanie obciążenia testów, ponieważ powiązania mogą nie być już dostępne do zastąpienia.

tddmonkey
źródło
7
Na przykład zdanie „szczekanie na niewłaściwe drzewo”: D
Boris Pavlović

Odpowiedzi:

149

To może nie być odpowiedź, której szukasz, ale jeśli piszesz testy jednostkowe, prawdopodobnie nie powinieneś używać wtryskiwacza, a raczej ręcznie wstrzykiwać fałszywe obiekty.

Z drugiej strony, jeśli naprawdę chcesz zastąpić pojedyncze wiązanie, możesz użyć 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()));

Zobacz szczegóły tutaj .

Ale jak Modules.overrides(..)zaleca javadoc for , powinieneś zaprojektować swoje moduły w taki sposób, aby nie trzeba było nadpisywać powiązań. W podanym przykładzie można to osiągnąć, przenosząc powiązanie programu InterfaceCdo osobnego modułu.

albertb
źródło
9
Dzięki Albert, to prowadzi mnie do robienia tego, co chcę. To jest jeszcze w wersji produkcyjnej! I to jest dla testów integracyjnych, a nie testów jednostkowych, dlatego chcę mieć pewność, że wszystko inne jest budowane poprawnie
tddmonkey,
1
Dodałem konkretny przykład do kodu. Czy to prowadzi cię dalej?
Albertb
1
O ile się nie mylę, ovveridegubi to, co właściwe Stage(czyli systematycznie używa się ROZWOJU).
pdeschen
4
Rozmiar ma znaczenie. Kiedy wykres zależności rośnie, ręczne okablowanie może być dość uciążliwe. Również w przypadku zmiany okablowania należy ręcznie zaktualizować wszystkie ręczne miejsca okablowania. Zastępowanie umożliwia automatyczną obsługę.
yoosiba,
3
@pdeschen To jest błąd w Guice 3, który naprawiłem dla Guice 4.
Tavian Barnes,
9

Dlaczego nie skorzystać z dziedziczenia? Możesz zastąpić określone powiązania w overrideMemetodzie, pozostawiając udostępnione implementacje w configuremetodzie.

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

I na koniec stwórz swój wtryskiwacz w ten sposób:

Guice.createInjector(new TestModule());
Mon Calamari
źródło
3
Wydaje się, @Overrideże nie działa. Zwłaszcza jeśli jest to zrobione metodą, że @Providescoś.
Sasanka Panguluri
4

Jeśli nie chcesz zmieniać modułu produkcyjnego i masz domyślną strukturę projektu podobną do Mavena, taką jak

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

Możesz po prostu utworzyć nową klasę ConcreteCw katalogu testowym, używając tego samego pakietu, co w przypadku oryginalnej klasy. Guice następnie połączy InterfaceCsię ConcreteCz twoim katalogiem testowym, podczas gdy wszystkie inne interfejsy zostaną powiązane z twoimi klasami produkcyjnymi.

Jan Gassen
źródło
2

Chcesz użyć Juckito, w którym możesz zadeklarować własną konfigurację dla każdej klasy testowej.

@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
źródło
1

W innej konfiguracji mamy więcej niż jedną aktywność zdefiniowaną w oddzielnych modułach. Działanie, które jest wstrzykiwane, znajduje się w module biblioteki systemu Android, z własną definicją modułu RoboGuice w pliku AndroidManifest.xml.

Konfiguracja wygląda następująco. W module bibliotecznym znajdują się następujące definicje:

AndroidManifest.xml:

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

Następnie mamy wstrzyknięty typ:

interface Foo { }

Niektóre domyślne implementacje Foo:

class FooThing implements Foo { }

MainModule konfiguruje implementację FooThing dla Foo:

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

I wreszcie działanie, które zużywa Foo:

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

W konsumującym module aplikacji na Androida chcielibyśmy użyć, SomeActivityale do celów testowych wstrzyknąć własne Foo.

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

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

Można by argumentować, aby ujawnić obsługę modułu aplikacji klienckiej, jednak musimy głównie ukryć wstrzykiwane składniki, ponieważ moduł biblioteki jest zestawem SDK, a ujawnianie elementów ma większe konsekwencje.

(Pamiętaj, to jest do testowania, więc znamy wewnętrzne cechy SomeActivity i wiemy, że zużywa (widoczny pakiet) Foo).

Sposób, w jaki to działa, ma sens; użyj sugerowanego zastąpienia do testowania :

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

Teraz, po SomeActivityuruchomieniu, otrzyma OtherFooThingdla swojej wstrzykniętej Fooinstancji.

Jest to bardzo specyficzna sytuacja, w której w naszym przypadku OtherFooThing był używany wewnętrznie do rejestrowania sytuacji testowych, podczas gdy FooThing był używany domyślnie do wszystkich innych zastosowań.

Należy pamiętać, że za pomocą #newDefaultRoboModulenaszych testów jednostkowych i działa bez zarzutu.

Dave T.
źródło