Creación de Servicios MOOC y Stub para Pruebas Unitarias en Swift

Clase 6 de 15Curso de Swift Unit Testing

Resumen

La programación de pruebas unitarias es un pilar fundamental en el desarrollo de software de calidad. Dominar técnicas como la creación de servicios simulados (mocks y stubs) te permitirá verificar el comportamiento de tu código de manera aislada y controlada, garantizando que cada componente funcione correctamente antes de integrarlo con el resto del sistema.

¿Qué son los servicios MOCK y STUB y cuál es su importancia en las pruebas unitarias?

Los servicios MOCK y STUB son herramientas esenciales para realizar pruebas unitarias efectivas en el desarrollo de aplicaciones. Ambos permiten simular comportamientos de componentes externos como bases de datos, lo que nos ayuda a aislar el código que queremos probar y tener un control total sobre el entorno de prueba.

La principal diferencia entre ellos radica en su complejidad y propósito:

  • STUB: Es una implementación simple que siempre devuelve la misma respuesta predefinida, sin lógica adicional. Son útiles cuando solo necesitamos simular un comportamiento básico.

  • MOCK: Es una implementación más avanzada que permite simular comportamientos más complejos, como contar cuántas veces se llama a una función o configurar diferentes respuestas según las circunstancias. Ofrecen mayor control y capacidad de verificación.

Estas herramientas son fundamentales porque nos permiten:

  • Realizar pruebas aisladas sin depender de componentes externos
  • Controlar exactamente qué datos se utilizan en las pruebas
  • Evitar la aleatoriedad, que es "el peor enemigo" de las pruebas unitarias
  • Simular diferentes escenarios, incluyendo casos de error

¿Cómo implementar un servicio MOCK para pruebas unitarias en Swift?

Para implementar un servicio MOCK efectivo en Swift, debemos crear una clase que implemente el mismo protocolo que el servicio real, pero con comportamientos controlados. Veamos cómo hacerlo paso a paso:

  1. Primero, necesitamos modificar nuestro servicio MOCK existente para eliminar cualquier comportamiento aleatorio:
// Eliminamos los MOCK Records aleatorios y agregamos variables controlables
var fetchRecordsResult: [ExpenseRecord] = []
var saveNewRecordResult: Bool = false
var updateRecordResult: Bool = false
var deleteRecordResult: Bool = false
var totalResult: (Double, Double) = (0, 0)
  1. Luego, implementamos los métodos del protocolo para que devuelvan estas variables controlables:
func fetchRecords() async throws -> [ExpenseRecord] {
    return fetchRecordsResult
}

func saveNewRecord(_ record: ExpenseRecord) async throws -> Bool {
    return saveNewRecordResult
}

func updateRecord(_ record: ExpenseRecord) async throws -> Bool {
    return updateRecordResult
}

func deleteRecord(_ record: ExpenseRecord) async throws -> Bool {
    return deleteRecordResult
}

func total() async throws -> (Double, Double) {
    return totalResult
}

La ventaja clave de este enfoque es que podemos modificar estas variables cada vez que creamos una instancia del MOCK, permitiéndonos controlar exactamente qué datos se utilizan en cada prueba.

¿Cómo crear un servicio STUB simple para pruebas básicas?

Los servicios STUB son aún más simples que los MOCK, ya que siempre devuelven los mismos valores predefinidos sin ninguna lógica adicional. Son perfectos para pruebas donde solo necesitamos que exista una implementación del protocolo, pero no nos importa realmente su comportamiento.

Para crear un STUB en Swift:

  1. Creamos una nueva clase que implemente el protocolo requerido:
class StubDatabaseService: DatabaseServiceProtocol {
    func fetchRecords() async throws -> [ExpenseRecord] {
        return []
    }
    
    func saveNewRecord(_ record: ExpenseRecord) async throws -> Bool {
        return false
    }
    
    func updateRecord(_ record: ExpenseRecord) async throws -> Bool {
        return false
    }
    
    func deleteRecord(_ record: ExpenseRecord) async throws -> Bool {
        return false
    }
    
    func total() async throws -> (Double, Double) {
        return (0, 0)
    }
}

Este STUB simplemente devuelve valores predeterminados: una lista vacía, falso para todas las operaciones y ceros para los totales. Es útil cuando necesitamos inyectar una dependencia que cumpla con el protocolo, pero que no vamos a utilizar directamente en la prueba específica.

¿Cómo configurar correctamente las pruebas unitarias para un ViewModel?

Una vez que tenemos nuestros servicios MOCK y STUB listos, necesitamos configurar nuestras pruebas unitarias adecuadamente. Esto implica crear una clase de prueba y configurar el entorno antes y después de cada prueba.

Configuración inicial de la clase de prueba

Para crear una clase de prueba efectiva:

  1. Creamos un nuevo archivo de prueba unitaria:
import XCTest
@testable import Gastify

class HomeViewModelTest: XCTestCase {
    // Propiedades que necesitaremos en nuestras pruebas
    var viewModel: HomeViewModel!
    var mockDatabaseService: MockDatabaseService!
    var stubDatabaseService: StubDatabaseService!
    
    // Configuración antes de cada prueba
    override func setUp() {
        super.setUp()
        mockDatabaseService = MockDatabaseService()
        stubDatabaseService = StubDatabaseService()
        // No inicializamos el viewModel aquí porque dependerá de cada prueba
    }
    
    // Limpieza después de cada prueba
    override func tearDown() {
        viewModel = nil
        mockDatabaseService = nil
        stubDatabaseService = nil
        super.tearDown()
    }
    
    // Aquí irán nuestros métodos de prueba
}

Es importante destacar que no inicializamos el ViewModel en el método setUp() porque en diferentes pruebas podríamos querer inyectarle diferentes servicios (el MOCK o el STUB), dependiendo de lo que estemos probando.

Beneficios de una buena configuración de pruebas

Esta estructura de pruebas nos proporciona varios beneficios:

  • Aislamiento: Cada prueba comienza con un estado limpio y controlado
  • Flexibilidad: Podemos configurar diferentes escenarios para cada prueba
  • Mantenibilidad: La estructura clara facilita añadir nuevas pruebas en el futuro
  • Confiabilidad: Evitamos efectos secundarios entre pruebas al limpiar el estado en tearDown()

La configuración adecuada de las pruebas es tan importante como las pruebas mismas, ya que garantiza que estamos probando exactamente lo que queremos probar, sin interferencias externas.

Los servicios MOCK y STUB son herramientas poderosas para crear pruebas unitarias efectivas. Al implementarlos correctamente, podemos probar nuestro código de manera aislada y controlada, lo que nos permite detectar problemas temprano en el ciclo de desarrollo. ¿Has utilizado estas técnicas en tus proyectos? Comparte tu experiencia en los comentarios y cuéntanos qué estrategias de prueba te han resultado más efectivas.