Enlace obligatorio en Guice

138

Acabo de empezar a jugar con Guice, y un caso de uso en el que puedo pensar es que en una prueba solo quiero anular un enlace único. Creo que me gustaría usar el resto de los enlaces de nivel de producción para asegurar que todo esté configurado correctamente y evitar duplicaciones.

Entonces imagina que tengo el siguiente 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);
    }
}

Y en mi prueba solo quiero anular InterfaceC, manteniendo InterfaceA e InterfaceB intactas, así que me gustaría algo como:

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

También probé lo siguiente, sin suerte:

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

¿Alguien sabe si es posible hacer lo que quiero o estoy ladrando por completo el árbol equivocado?

--- Seguimiento: Parece que puedo lograr lo que quiero si uso la etiqueta @ImplementedBy en la interfaz y luego solo proporciono un enlace en el caso de prueba, que funciona bien cuando hay un mapeo 1-1 entre La interfaz y la implementación.

Además, después de discutir esto con un colega, parecería que deberíamos anular un módulo completo y asegurarnos de que nuestros módulos estén definidos correctamente. Sin embargo, parece que podría causar un problema cuando un enlace está fuera de lugar en un módulo y necesita ser movido, por lo que posiblemente se rompa una carga de pruebas ya que los enlaces ya no estarán disponibles para anularse.

tddmonkey
fuente
77
Como la frase "ladrando el árbol equivocado": D
Boris Pavlović

Respuestas:

149

Puede que esta no sea la respuesta que está buscando, pero si está escribiendo pruebas unitarias, probablemente no debería estar usando un inyector y más bien estar inyectando objetos falsos o falsos a mano.

Por otro lado, si realmente desea reemplazar un enlace único, puede 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()));

Ver detalles aquí .

Pero como Modules.overrides(..)recomienda Javadoc , debe diseñar sus módulos de tal manera que no necesite anular los enlaces. En el ejemplo que dio, puede lograrlo moviendo el enlace InterfaceCa un módulo separado.

albertb
fuente
9
Gracias Albert, eso me lleva por el camino de hacer lo que quiero. ¡Eso está en un lanzamiento de producción todavía! Y esto es para pruebas de integración, no para pruebas unitarias, por lo que quiero asegurarme de que todo lo demás se construya correctamente
Tddmonkey
1
He agregado un ejemplo concreto al código. ¿Te lleva más lejos?
albertb
1
A menos que me equivoque, ovveridepierde el correcto Stageal hacerlo (es decir, DESARROLLO se usa sistemáticamente).
pdeschen
44
El tamaño importa. Cuando crece el gráfico de dependencia, el cableado manual puede ser bastante complicado. Además, cuando cambie el cableado, debe actualizar manualmente todos sus lugares de cableado manual. La anulación le permite manejar eso automáticamente.
yoosiba
3
@pdeschen Eso es un error en Guice 3 que arreglé para Guice 4.
Tavian Barnes
9

¿Por qué no usar la herencia? Puede anular sus enlaces específicos en el overrideMemétodo, dejando implementaciones compartidas en el 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);
    }
}

Y finalmente cree su inyector de esta manera:

Guice.createInjector(new TestModule());
Mon Calamari
fuente
3
El @Overrideno parece funcionar. Especialmente si se hace en un método que @Providesalgo.
Sasanka Panguluri
4

Si no desea cambiar su módulo de producción y si tiene una estructura de proyecto maven predeterminada como

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

Puede crear una nueva clase ConcreteCen su directorio de prueba usando el mismo paquete que para su clase original. Guice se unirá entonces InterfaceCa ConcreteCpartir de su directorio de prueba, mientras que todas las demás interfaces se unirán a sus clases de producción.

Jan Gassen
fuente
2

Desea usar Juckito donde puede declarar su configuración personalizada para cada clase de prueba.

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

En una configuración diferente, tenemos más de una actividad definida en módulos separados. La actividad que se está inyectando está en un módulo de biblioteca de Android, con su propia definición de módulo RoboGuice en el archivo AndroidManifest.xml.

La configuración se ve así. En el módulo de biblioteca hay estas definiciones:

AndroidManifest.xml:

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

Luego tenemos un tipo que se inyecta:

interface Foo { }

Alguna implementación predeterminada de Foo:

class FooThing implements Foo { }

MainModule configura la implementación de FooThing para Foo:

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

Y finalmente, una actividad que consume Foo:

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

En el consumo del Módulo de aplicaciones de Android, nos gustaría usar SomeActivitypero, para fines de prueba, inyectar el nuestro Foo.

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

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

Se podría argumentar que exponer el manejo del módulo a la aplicación del cliente, sin embargo, necesitamos ocultar principalmente los componentes que se inyectan porque el Módulo de biblioteca es un SDK, y exponer piezas tiene implicaciones más grandes.

(Recuerde, esto es para pruebas, por lo que conocemos los aspectos internos de SomeActivity y sabemos que consume un Foo (paquete visible)).

La forma en que descubrí que funciona tiene sentido; use la anulación sugerida para probar :

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

Ahora, cuando SomeActivityse inicia, obtendrá OtherFooThingsu Fooinstancia inyectada .

Es una situación muy específica en la que, en nuestro caso, OtherFooThing se usó internamente para registrar situaciones de prueba, mientras que FooThing se usó, por defecto, para todos los demás usos.

Tenga en cuenta que estamos utilizando #newDefaultRoboModuleen nuestras pruebas unitarias, y funciona perfectamente.

Dave T.
fuente