«Primero la prueba — luego el código»: ¿por qué funciona en Swift?
\n\n¿Alguna vez ha reescrito el mismo módulo por tercera vez porque «había que lanzarlo urgentemente»? ¿O ha encontrado un error en producción que era más fácil prevenir en la fase de escritura? Si es así — bienvenido al mundo de TDD (Test-Driven Development).
\n\nEl desarrollo guiado por pruebas no es solo una metodología de moda. Es una forma de pensar que le obliga a formular cómo debe comportarse el código antes de escribirlo. En Swift, con su tipado estricto y el potente framework XCTest, TDD se convierte en un escudo fiable contra regresiones y caídas inesperadas.
En este artículo analizaremos TDD con ejemplos reales, escribiremos pruebas para la capa de red y el modelo de datos, y, lo más importante, entenderemos cómo implementar esta práctica sin dolor. ¡Allá vamos!
\n\nRojo-Verde-Refactorización: la magia del ciclo
\n\nLa base de TDD es un ciclo de tres pasos:
\n\n- \n
- Rojo (Red) — escribimos una prueba que no pasa (porque el código aún no existe). \n
- Verde (Green) — escribimos el código mínimo para que la prueba pase. \n
- Refactorización (Refactor) — mejoramos el código sin cambiar su comportamiento (las pruebas nos protegen). \n
¿Parece sencillo? Probemos en Swift.
\n\nPrimera prueba: verificamos el modelo User
\n\nSupongamos que tenemos un modelo simple de usuario. Queremos que formatee correctamente el nombre completo. Primero — la prueba:
\n\nimport XCTest\n@testable import MyApp\n\nfinal class UserTests: XCTestCase {\n \n func testFullName_GivenFirstNameAndLastName_ReturnsFormattedString() {\n // Arrange\n let user = User(firstName: "Ana", lastName: "Karenina")\n \n // Act\n let fullName = user.fullName()\n \n // Assert\n XCTAssertEqual(fullName, "Ana Karenina", "El nombre completo debe consistir en nombre y apellido separados por un espacio")\n }\n}\n\n\nEjecutamos — la prueba falla (rojo). El compilador se queja de que User no existe. Perfecto, así está previsto.
Ahora escribimos el código mínimo para que la prueba pase:
\n\nstruct User {\n let firstName: String\n let lastName: String\n \n func fullName() -> String {\n return "\\(firstName) \\(lastName)"\n }\n}\n\n\nEjecutamos — ¡verde! Podemos pasar a la refactorización. Por ejemplo, añadir manejo de cadenas vacías o un segundo apellido. Pero no se apresure — las pruebas le indicarán cuándo es necesario.
\n\nProbando la capa de red asíncrona
\n\nUno de los problemas más comunes en el desarrollo de iOS es trabajar con la red. TDD es especialmente útil aquí porque separa explícitamente la lógica de las llamadas de red.
\n\nEscribimos un protocolo y un objeto mock
\n\nPrimero definimos cómo debería verse nuestro servicio:
\n\nprotocol WeatherServiceProtocol {\n func fetchWeather(for city: String, completion: @escaping (Result<Weather, Error>) -> Void)\n}\n\n\nAhora — la prueba. No queremos depender de una API real, así que creamos un mock:
\n\nclass MockWeatherService: WeatherServiceProtocol {\n var shouldReturnError = false\n var mockWeather: Weather?\n \n func fetchWeather(for city: String, completion: @escaping (Result<Weather, Error>) -> Void) {\n if shouldReturnError {\n completion(.failure(NetworkError.serverError))\n } else if let weather = mockWeather {\n completion(.success(weather))\n }\n }\n}\n\nfinal class WeatherViewModelTests: XCTestCase {\n \n func testFetchWeather_Success_UpdatesModel() {\n // Arrange\n let mockService = MockWeatherService()\n mockService.mockWeather = Weather(temperature: 25.0, condition: "sunny")\n let viewModel = WeatherViewModel(service: mockService)\n let expectation = XCTestExpectation(description: "Esperando la carga del clima")\n \n