Substituindo Vinculação no Guice

138

Acabei de começar a brincar com o Guice, e um caso de uso em que consigo pensar é que, em um teste, quero substituir uma única ligação. Eu acho que gostaria de usar o restante das ligações de nível de produção para garantir que tudo esteja configurado corretamente e para evitar duplicação.

Então imagine que eu tenho o seguinte módulo

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

E no meu teste, eu quero apenas substituir a InterfaceC, mantendo a InterfaceA e a InterfaceB intactas, então gostaria de algo como:

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

Eu também tentei o seguinte, sem sorte:

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

Alguém sabe se é possível fazer o que eu quero ou estou latindo completamente na árvore errada?

--- Acompanhamento: parece que posso conseguir o que quero se fizer uso da tag @ImplementedBy na interface e fornecer apenas uma ligação no caso de teste, o que funciona bem quando há um mapeamento 1-1 entre a interface e implementação.

Além disso, depois de discutir isso com um colega, parece que seguiríamos no caminho de substituir um módulo inteiro e garantir que nossos módulos estejam definidos corretamente. Parece que isso pode causar um problema, embora uma ligação seja extraviada em um módulo e precise ser movida, possivelmente interrompendo uma carga de testes, pois as ligações podem não estar mais disponíveis para serem substituídas.

tddmonkey
fonte
7
Como a frase "latindo na árvore errada": D
Boris Pavlović

Respostas:

149

Pode não ser a resposta que você está procurando, mas se estiver escrevendo testes de unidade, provavelmente não deve usar um injetor e sim injetar objetos falsos ou falsos à mão.

Por outro lado, se você realmente deseja substituir uma única ligação, pode usar 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()));

Veja detalhes aqui .

Mas, como Modules.overrides(..)recomenda o javadoc for , você deve projetar seus módulos de forma que não precise substituir ligações. No exemplo que você deu, você pode conseguir isso movendo a ligação de InterfaceCpara um módulo separado.

albertb
fonte
9
Obrigado Albert, que me leva de alguma maneira no caminho para fazer o que eu quero. Isso já está em um lançamento de produção! E isto é para testes de integração, e não testes de unidade, que eu por que eu quero para garantir que tudo mais está sendo construído corretamente
tddmonkey
1
Adicionei um exemplo concreto ao código. Isso o leva mais adiante?
albertb
1
A menos que eu esteja enganado, ovverideperde o devido Stageao fazê-lo (isto é, o DESENVOLVIMENTO é usado sistematicamente).
pdeschen
4
Tamanho importa. Quando o gráfico de dependência aumenta, a conexão manual pode ser bastante trabalhosa. Além disso, quando a fiação for alterada, você precisará atualizar manualmente todos os seus locais de fiação manual. A substituição permite que você lide com isso automaticamente.
yoosiba
3
@pdeschen Isso é um erro no Guice 3 que eu fixo para Guice 4.
Tavian Barnes
9

Por que não usar herança? Você pode substituir suas ligações específicas no overrideMemétodo, deixando implementações compartilhadas no configuremétodo

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

E, finalmente, crie seu injetor desta maneira:

Guice.createInjector(new TestModule());
Mon Calamari
fonte
3
O @Overrideparece não funcionar. Especialmente se for feito em um método que @Providesalgo.
Sasanka Panguluri
4

Se você não deseja alterar seu módulo de produção e se possui uma estrutura de projeto padrão semelhante a um maven, como

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

Você pode apenas criar uma nova classe ConcreteCno seu diretório de teste usando o mesmo pacote da sua classe original. O Guice será vinculado InterfaceCao ConcreteCdiretório de teste, enquanto todas as outras interfaces serão vinculadas às suas classes de produção.

Jan Gassen
fonte
2

Você deseja usar o Juckito, onde pode declarar sua configuração personalizada para cada classe de teste.

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

Em uma configuração diferente, temos mais de uma atividade definida em módulos separados. A atividade que está sendo injetada está em um Android Library Module, com sua própria definição de módulo RoboGuice no arquivo AndroidManifest.xml.

A configuração é assim. No módulo Library, existem estas definições:

AndroidManifest.xml:

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

Então temos um tipo sendo injetado:

interface Foo { }

Alguma implementação padrão do Foo:

class FooThing implements Foo { }

MainModule configura a implementação do FooThing para o Foo:

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

E, finalmente, uma atividade que consome Foo:

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

No consumidor Android Application Module, gostaríamos de usar SomeActivity, mas, para fins de teste, injetar o nosso Foo.

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

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

Alguém pode argumentar para expor o manuseio do módulo ao aplicativo cliente, no entanto, precisamos ocultar principalmente os componentes que estão sendo injetados porque o Módulo Biblioteca é um SDK, e a exposição de peças tem implicações maiores.

(Lembre-se, isso é para teste, por isso conhecemos os elementos internos do SomeActivity e sabemos que ele consome um Foo (pacote visível)).

A maneira como descobri que funciona faz sentido; use a substituição sugerida para testar :

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

Agora, quando SomeActivityiniciado, ele obterá OtherFooThingsua Fooinstância injetada .

É uma situação muito específica em que, no nosso caso, o OtherFooThing foi usado internamente para registrar situações de teste, enquanto o FooThing foi usado, por padrão, para todos os outros usos.

Lembre-se de que estamos usando #newDefaultRoboModulenossos testes de unidade e funciona perfeitamente.

Dave T.
fonte