Esta pagina se ve mejor con JavaScript habilitado

Mocks, Fakes y Más

 ·  ☕ 4 minutos lectura  ·  ✍️ Ignacio Carrión

Mocks, Fakes y Más: Entendiendo los Test Doubles en Kotlin

Al escribir tests en Kotlin, especialmente para el desarrollo de Android, a menudo necesitamos reemplazar dependencias reales con test doubles. Sin embargo, no todos los test doubles son iguales: términos como mocks, fakes, stubs, spies y dummies aparecen con frecuencia. En esta publicación, desglosaremos sus diferencias con ejemplos en Kotlin utilizando solo Kotlin puro (sin bibliotecas de terceros).


1. Entendiendo los Test Doubles

Los test doubles son objetos que sustituyen dependencias reales en las tests. Ayudan a aislar el sistema bajo prueba (SUT) y hacen que las tests sean más confiables. Aquí están los principales tipos:

  • Dummy – Un objeto de relleno que se pasa como argumento pero no se utiliza en la ejecución del test.
  • Stub – Proporciona respuestas predefinidas pero no contiene lógica.
  • Fake – Una implementación liviana con lógica en memoria.
  • Mock – Un test double que verifica interacciones.
  • Spy – Envuelve un objeto real permitiendo la modificación selectiva de su comportamiento.

2. Objetos Dummy

Un dummy es un objeto que solo existe para satisfacer la firma de un método, pero nunca se usa realmente.

Ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface EmailSender {
    fun sendEmail(email: String, message: String)
}

class UserService(private val emailSender: EmailSender) {
    fun registerUser(user: User) {
        // The user registers, but we don't actually send an email
    }
}

data class User(val name: String, val email: String)

// Test
fun testRegisterUser() {
    val dummyEmailSender = object : EmailSender {
        override fun sendEmail(email: String, message: String) {
            // This will never be called in the test
        }
    }
    val userService = UserService(dummyEmailSender)

    userService.registerUser(User("John Doe", "john@example.com"))
}

3. Stubs

Un stub devuelve respuestas predefinidas a las llamadas a métodos, pero no rastrea interacciones.

Ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
interface UserRepository {
    fun getUser(id: Int): User?
}

class StubUserRepository : UserRepository {
    override fun getUser(id: Int): User? {
        return if (id == 1) User("John Doe", "john@example.com") else null
    }
}

// Test
fun testGetUser() {
    val stubRepo = StubUserRepository()
    val user = stubRepo.getUser(1)

    assert(user?.name == "John Doe")
}

4. Fakes

Un fake es una versión simplificada pero funcional de una clase real, a menudo utilizando almacenamiento en memoria.

Ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class FakeUserRepository : UserRepository {
    private val users = mutableMapOf<Int, User>()

    override fun getUser(id: Int): User? = users[id]

    fun addUser(id: Int, user: User) {
        users[id] = user
    }
}

// Test
fun testFakeUserRepository() {
    val fakeRepo = FakeUserRepository()
    fakeRepo.addUser(1, User("Jane Doe", "jane@example.com"))

    assert(fakeRepo.getUser(1)?.name == "Jane Doe")
}

5. Mocks

Un mock es un test double que verifica interacciones. Sin un framework de mocking, debemos rastrear manualmente las llamadas.

Ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class MockEmailSender : EmailSender {
    var wasSendEmailCalled = false
    var sentTo: String? = null
    var sentMessage: String? = null

    override fun sendEmail(email: String, message: String) {
        wasSendEmailCalled = true
        sentTo = email
        sentMessage = message
    }
}

// Test
fun testSendWelcomeEmail() {
    val mockEmailSender = MockEmailSender()
    val service = NotificationService(mockEmailSender)

    service.sendWelcomeEmail(User("test@example.com", "test@example.com"))

    assert(mockEmailSender.wasSendEmailCalled)
    assert(mockEmailSender.sentTo == "test@example.com")
    assert(mockEmailSender.sentMessage == "Welcome!")
}

class NotificationService(private val emailSender: EmailSender) {
    fun sendWelcomeEmail(user: User) {
        emailSender.sendEmail(user.email, "Welcome!")
    }
}

6. Spies

Un spy envuelve un objeto real mientras permite la modificación selectiva de su comportamiento. Sin una biblioteca, debemos extender la clase real y sobrescribir comportamientos específicos.

Ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
open class MathService {
    open fun add(a: Int, b: Int) = a + b
}

class SpyMathService : MathService() {
    var wasAddCalled = false
    var lastA: Int? = null
    var lastB: Int? = null

    override fun add(a: Int, b: Int): Int {
        wasAddCalled = true
        lastA = a
        lastB = b
        return super.add(a, b) // Calls the real implementation
    }
}

7. Uso de MockK para Mocks y Spies

Si bien es posible crear mocks y spies manualmente, usar una biblioteca como MockK simplifica el proceso.

Ejemplo usando MockK:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import io.mockk.*

fun testMockKExample() {
    val mockEmailSender = mockk<EmailSender>()
    every { mockEmailSender.sendEmail(any(), any()) } just Runs

    val service = NotificationService(mockEmailSender)
    service.sendWelcomeEmail(User("test@example.com", "test@example.com"))

    verify { mockEmailSender.sendEmail("test@example.com", "Welcome!") }
}

MockK proporciona características avanzadas como spies automáticos, mocks relajados y captura de argumentos, lo que facilita la escritura de tests mantenibles.


Conclusión

Comprender los test doubles te ayuda a escribir mejores tests al aislar dependencias. Usa:

Dummies cuando se requiere un argumento pero no se usa.
Stubs para devolver valores predefinidos.
Fakes para implementaciones livianas.
Mocks para verificar interacciones.
Spies cuando necesitas mockeo parcial.
MockK para un mockeo más fácil y potente.

Al elegir el tipo correcto, puedes hacer que tus tests sean más confiables y mantenibles.

compartir en

Ignacio Carrión
Escrito por
Ignacio Carrión
Android Developer