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 :

  1. Reutilización de código

  2. Facilidad de refactorización

  3. Facilidad de testing

Veamos un Ejemplo

  1. Creamos una clase Car con una dependencia de otra clase , por ejemplo Engine

class Car{

    private val engine = Engine()



    fun start() : String {

        if (engine.start() == "ON"){

            return "ON"

        }else{

            return "OFF"

        }

    }

}
  1. Creamos la clase Engine

class Engine{



    private var state = "OFF"



    fun start(): String{

        this.state = "ON"

        return this.state

    }

}
  1. Esto no es inyección de dependencias, ya que Car

    crear su propia instancia de Engine

  1. Esto puede ser problematico por:
  1. Car y Engine están estrechamente acoplados.
  1. Una instancia de Car usa un tipo de Engine y no se pueden usar subclases

    o implementaciones alternativas.

  1. Si Car construye su propio Engine,
  1. 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

  1. La dependencia dura con Engine , genera dificultada al hacer test.
  1. Como Car usa una instancia real de Engine
  1. Esto no permite usar un doble de prueba
  1. Para modificar Engine para diferentes casos de prueba
  1. Ahora tratemos de hacer un test

class CarTest{



    @Test

    fun carStart(){



        val sut = Car()



        assertEquals("ON",sut.start())



    }

}
  1. 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())



    }

}
  1. ¿ Qué pasa aquí ?

  1. 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())

}
  1. ¿ Que paso ?
  1. La función main usa Car
  1. Como Car , necesita Engine
  1. Y con esto se puede construir una instancia de Car
  1. 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())



    }

}
  1. Los beneficios son:
  1. Reutilización de Car
  1. Se pueden pasar multiples instancias de Engine a Car
  1. Podemos usar una subclass llamada ElectricEngine , por ejemplo
  1. Car es fácil de testear, se puede pasar engine y comprobar multiples escenarios
  1. Por ejemplo podemos crear un FakeEngine

  1. Hay dos formas principales de hacer inyección de dependencias en Android
  1. Constructor Injection, es la técnica mostrada anteriormente
  1. Field Injection (or Setter Injection)
  1. En Android algunas clases son instanciadas por el Sistema, como los activity
  1. Por lo tanto nos es posible realizar inyección por el constructor
  1. 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()

}
  1. 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

  1. En los ejemplos anteriores, realizamos la inyección de dependencias de forma manual
  1. Pero la inyección de dependencias manual, tiene sus problemas
  1. 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

  1. Por reflexión, donde se conectan las dependencias en runtime

  2. Soluciones estáticas que generan código que conectan las dependencias en tiempo de compilación

Dagger

  1. Es una popular librería para la inyección de dependencias, para Java, Kotlin y Android

    que es mantenido por Google.

  2. Dagger facilita la inyección de dependencias, creando y administrando el gráfico de dependencias por tí

  3. 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

  1. Creas una clase, que es conocida como service locator

  2. 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() )

}
  1. A diferencia de la inyección de dependencias, las clases tiene el control

    y consultan por las dependencias que necesitan.

  1. 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

  1. 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.

  1. 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

  1. 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

  1. Reusability

  2. Facil refactoring

  3. Facil de testing