Tests unitarios en Android con Mockk y Truth (II): Funciones avanzadas de los mocks

Introducción

En este artículo se explican conceptos de mocking un poco más avanzados para realizar tests unitarios en Android utilizando las librerías Mockk y Truth.

Testeando cambios en los campos de una clase mockeada

Muchas veces, necesitamos comprobar por alguna razón u otra que cuando llamamos a un método del SUT (subject under test), algo en una de sus dependencias (que tenemos mockeadas) cambia.

Veamos el siguiente ejemplo: siguiendo con las clases Car y Engine del ejemplo anterior (en el que todo coche depende de un motor), podemos querer comprobar que, cuando encendemos el coche, el motor pasa a estado "encendido".

Sin embargo, nosotros no tenemos un objeto Engine como tal, sino un mock del mismo, por lo que, si intentamos obtener el valor de isStarted, vamos a obtener una excepción en nuestro test. Para esto se utiliza la keyword capture:

class CarTest {
    lateinit var engine: Engine
    lateinit var car: Car

    @Before
    fun setUp() {
        engine = mockk<Engine>() 
        car = Car (engine)
    }

    @After
    fun tearDown() {
        unmockkAll()
    }

    @Test
    fun `Engine turns on when car turns on`() {
        val engineStarted = slot<Boolean>()
        every { engine.started = capture(engineStarted) } just runs
        car.start()
        assertThat(engineStarted.captured).isTrue()
    }
}

Es decir, al igual que con cualquier otro método de un mock, necesitamos de hecho definir el comportamiento de sus setters, si es que estos van a utilizarse. Y mediante la keyword capture podemos saber el valor que obtendrían después de ejecutarse el método que sea.

De igual manera, si necesitásemos solo hacer que el setter funcione pero no nos interesa especialmente el valor, podríamos hacer lo siguiente:

every { engine.started = any() } just runs

Es importante hacer esto, porque si se utiliza el setter y no hemos definido su comportamiento, el test lanzará una excepción.

Testeando con clases estáticas y companion objects

A veces, es posible que tengamos que hacer uso de clases estáticas en nuestro código, como la librería Math o la muy común clase de Log de Android. Si nuestro código hace uso de alguna clase estática vemos que se nos lanzará una excepción en el test diciendo que no hemos mockeado su comportamiento.

Sin embargo, no podemos llamar a la función mockk, puesto que no podemos crear instancias de una clase estática. Entonces, ¿cómo lo hacemos?

La respuesta es muy sencilla, podemos utilizar mockkStatic:

class CarTest {
    @Before
    fun setUp() {
        mockkStatic(Log::class)
        every { Log.d(any(), any()) } just runs
    }    

    @After
    fun tearDown() {
        unmockkStatic()
    }
}

Puesto que los companion objects de Kotlin vienen a ser el equivalente en Java a una clase estática, podríamos estar inclinados a pensar que esto mismo nos sirve para ellos. Sin embargo, el concepto de static no existe realmente en Kotlin y, aunque se le parecen, los companion objects tampoco son clases estáticas, por lo que no nos sirve esta aproximación.

Nuevamente, la librería tiene una función justo para esto y es mockkObject:

@Before
fun setUp() {
    mockkObject(MyCompanionObject.Companion)
    every { MyCompanionObject.doSomething() } just runs
}

Con lo aprendido en el anterior artículo de la serie y este, ya prácticamente sabemos crear mocks de casi cualquier cosa. En los siguientes artículos se trabajará con tests de conceptos más complejos de Kotlin y Android, como corrutinas, Flows, LiveData...