Introducción a los Test
Inyección de dependencias
La inyección de dependencias es una técnica ampliamente utilizada
en programación y muy adecuada para el desarrollo en android
Las ventajas son :
Reutilización de código
Facilidad de refactorización
Facilidad de testing
Veamos un Ejemplo
- Creamos una clase
Car
con una dependencia de otra clase , por ejemploEngine
class Car{
private val engine = Engine()
fun start() : String {
if (engine.start() == "ON"){
return "ON"
}else{
return "OFF"
}
}
}
- Creamos la clase
Engine
class Engine{
private var state = "OFF"
fun start(): String{
this.state = "ON"
return this.state
}
}
Esto no es inyección de dependencias, ya que
Car
crear su propia instancia de
Engine
- Esto puede ser problematico por:
Car
yEngine
están estrechamente acoplados.
Una instancia de
Car
usa un tipo deEngine
y no se pueden usar subclaseso implementaciones alternativas.
- Si
Car
construye su propioEngine
,
Tendría que crear dos tipos de automóviles
en lugar de reutilizar el mismo automóvil, para motores de tipo Gas y Eléctrico
- La dependencia dura con
Engine
, genera dificultada al hacer test.
- Como
Car
usa una instancia real deEngine
- Esto no permite usar un doble de prueba
- Para modificar
Engine
para diferentes casos de prueba
- Ahora tratemos de hacer un test
class CarTest{
@Test
fun carStart(){
val sut = Car()
assertEquals("ON",sut.start())
}
}
- Ahora tratemos de cambiar el comportamiento de
Engine
class CarTest{
@Test
fun carStart(){
val sut = Car()
assertEquals("ON",sut.start())
}
@Test
fun quePasaAhoraConEngineOFF(){
val engine = mockk<Engine>()
every { engine.start() } returns "OFF"
val sut = Car()
assertEquals("OFF",sut.start())
}
}
- ¿ Qué pasa aquí ?
- Modifiquemos un poco el código
class SuperCar(private val engine: Engine) {
fun start() : String {
return engine.start()
}
}
fun main(){
val engine = Engine()
val superCar = SuperCar(engine)
println (superCar.start())
}
- ¿ Que paso ?
- La función
main
usaCar
- Como
Car
, necesitaEngine
- Y con esto se puede construir una instancia de
Car
- Ahora vamos por el test
class SuperCarTest {
@Test
fun caminoFeliz(){
val engine = mockk<Engine>()
every { engine.start() } returns "ON"
val sut = SuperCar(engine)
assertEquals("ON",sut.start())
}
@Test
fun caminoNoFeliz(){
val engine = mockk<Engine>()
every { engine.start() } returns "OFF"
val sut = SuperCar(engine)
assertEquals("OFF",sut.start())
}
}
- Los beneficios son:
- Reutilización de Car
- Se pueden pasar multiples instancias de Engine a Car
- Podemos usar una subclass llamada ElectricEngine , por ejemplo
Car
es fácil de testear, se puede pasar engine y comprobar multiples escenarios
- Por ejemplo podemos crear un
FakeEngine
- Hay dos formas principales de hacer inyección de dependencias en Android
- Constructor Injection, es la técnica mostrada anteriormente
- Field Injection (or Setter Injection)
- En Android algunas clases son instanciadas por el Sistema, como los activity
- Por lo tanto nos es posible realizar inyección por el constructor
- Con Field Injection, las dependencias se instancias después que se crea la clase
class FieldCar{
lateinit var engine : Engine
fun start():String{
if (engine.start() == "ON"){
return "ON"
}else{
return "OFF"
}
}
}
fun main(){
val fieldCard = FieldCar()
fieldCard.engine = Engine()
fieldCard.start()
}
- Test
class FieldCarTest {
@Test
fun caminoFeliz(){
val engine = mockk<Engine>()
every { engine.start() } returns "ON"
val sut = FieldCar()
sut.engine = engine
assertEquals("ON",sut.start())
}
@Test
fun caminoNoFeliz(){
val engine = mockk<Engine>()
every { engine.start() } returns "OFF"
val sut = FieldCar()
sut.engine = engine
assertEquals("OFF",sut.start())
}
}
Automatización de inyección de dependencias
- En los ejemplos anteriores, realizamos la inyección de dependencias de forma manual
- Pero la inyección de dependencias manual, tiene sus problemas
Para grandes aplicaciones, tomar todas las dependencias y conectar estas correctamente
requiere generar una gran cantidad de Boilerplate code
En una arquitectura por capas, si creamos un objeto de una capa superior, se deben proporcionar
todas las dependencias de objetos de capas inferiores.
Por ejemplo, para construir un automóvil real, es posible que necesite un motor,
una transmisión, un chasis y otras piezas y un motor, necesitas cilindros y bujías
Hay dos categorías en librerías que resuelven este problema
Por reflexión, donde se conectan las dependencias en runtime
Soluciones estáticas que generan código que conectan las dependencias en tiempo de compilación
Dagger
Es una popular librería para la inyección de dependencias, para Java, Kotlin y Android
que es mantenido por Google.
Dagger facilita la inyección de dependencias, creando y administrando el gráfico de dependencias por tí
Proporciona dependencias totalmente estáticas y en tiempo de compilación
que abordan muchos de los problemas de desarrollo y rendimiento de soluciones basadas en la reflexión
Alternativas a la inyección de dependencias
Una alternativa a la inyección de dependencias, es usar un service locator
Creas una clase, que es conocida como service locator
Esta crea y almacena las dependencias y depués las proporciona on-demand
object ServiceLocator{
fun getEngine(): Engine = Engine()
}
class ServiceLocatorCar {
private val engine = ServiceLocator.getEngine()
fun start():String{
return engine.start()
}
}
fun main(){
val car = ServiceLocatorCar()
println( car.start() )
}
A diferencia de la inyección de dependencias, las clases tiene el control
y consultan por las dependencias que necesitan.
En la inyección de dependencias es la aplicación tiene el control
e inyecta proactivamente los objetos requeridos
Comparada con la inyección de dependencias
La recopilación de dependencias requeridas por un service locator
hace que el código sea más difícil de probar por que todas las pruebas
tienen que interactuar con el mismo service locator global.
Las dendencias están codificadas en la implementación de la clase,
por lo tanto, es más difícil saber qué necesita una clase desde afuera
Como resultado, los cambios en Car o en las dependencias del service locator, pueden
ocasionar *fallas en tiempo de ejecución o en las pruebas al hacer que las
referencias fallen.*
En resumen
Ventajas de la inyección de dependencias
Reusability
Facil refactoring
Facil de testing