Artículo
Patrón de dobles en Swift
Aprende a implementar el patrón de dobles en Swift puro para testing y previews instantáneas en SwiftUI, sin librerías de terceros. Protocolos, inyección de dependencias y código testable usando solo las capacidades nativas del lenguaje.
El Patrón de Dobles en Swift: Testing y Previews sin Librerías de Terceros
Hablemos de uno de esos patrones que separan al desarrollador que copia código del que entiende lo que hace: el patrón de dobles. Y no, no hablo de acrobacias de cine, hablo de cómo escribir código testable, tener previews en SwiftUI sin esperar 3 minutos a que muestre el resultado y hacerlo sin acudir a librerías de terceros ni frameworks de inyección de dependencias complejos. Todo con código Swift puro y duro.
🎯 El problema que nadie te cuenta
Imagina esto: estás desarrollando una app que carga películas desde una API. Cada vez que quieres ver la vista en el Canvas de Xcode tienes que esperar a que la preview compile, se conecte al servidor, descargue datos... y si no tienes internet o el servidor está caído, directamente la preview falla. Además, cuando quieres hacer tests unitarios de tu lógica de negocio, estás haciendo peticiones reales a un servidor, lo cual es lento, impredecible y totalmente ineficiente.
Lo típico es que mucha gente acuda a librerías de terceros para gestionar esto. Pero como siempre digo: si hay una solución nativa, úsala. Y en este caso la hay: protocolos + inyección de dependencias. Simple, elegante, Swift puro.
🧠 El concepto: acoplamiento leve mediante protocolos
Los protocolos en Swift son contratos. Definen qué debe hacer algo, pero no cómo lo hace. Esto es acoplamiento leve: tu lógica de negocio no depende de una implementación concreta, sino de un contrato. Y aquí es donde entra el patrón de dobles: crear implementaciones alternativas que cumplan el mismo contrato pero con comportamientos diferentes según el contexto.
Vamos con el ejemplo práctico. Primero definimos nuestro contrato:
protocol DataRepository {
func load() async throws -> [Movie]
}Este protocolo dice: "quien implemente esto debe tener un método load() que devuelve un array de películas". Punto. No dice nada de CÓMO las carga. Pueden venir de una API, de una base de datos local, de un archivo JSON o de datos mockeados. Eso es problema de quien implemente el protocolo, no del que lo usa.
👨🏻💻 La implementación real: la que usarás en producción
struct Repository: DataRepository {
func load() async throws -> [Movie] {
// Aquí harías la llamada real a tu API
let url = URL(string: "https://api.tuservidor.com/movies")!
let data = try await URLSession.shared.data(from: url)
let movies = try JSONDecoder().decode([Movie].self, from: data.0)
return movies
}
}Y ahora el doble para testing: rápido, predecible, sin dependencias externas
struct RepositoryTest: DataRepository {
func load() async throws -> [Movie] {
[Movie(id: 1, title: "Inception", year: 2010),
Movie(id: 2, title: "The Matrix", year: 1999),
Movie(id: 3, title: "Interstellar", year: 2014),
Movie(id: 4, title: "Blade Runner", year: 1982),
Movie(id: 5, title: "Arrival", year: 2016)]
}
}Aquí viene la magia. Fíjate en esta clase (que en SwiftUI sería @Observable u ObservableObject según uses el nuevo modelo de observabilidad o el antiguo)
@Observable
final class Presentation {
var movies: [Movie]
let repository: DataRepository // Fíjate: usamos el PROTOCOLO
init(repository: DataRepository = Repository()) {
self.repository = repository
self.movies = repository.load()
}
}🤯 ¿Ves la jugada? El constructor recibe un DataRepository (el protocolo), no un Repository concreto. Por defecto usa Repository() (la implementación real), pero podemos inyectarle cualquier cosa que cumpla el protocolo. Esto es inyección de dependencias: no creamos las dependencias dentro de la clase, las recibimos desde fuera.
🎪 Casos de uso prácticos
Caso 1: Producción
let presentation = Presentation()
// Usa Repository() por defecto, carga datos reales
print(presentation.movies.count) // 3.000 películas desde tu APICaso 2: Testing
let presentationTest = Presentation(repository: RepositoryTest())
// Usa el doble, sin tocar red ni base de datos
print(presentationTest.movies.count) // 5 películas de pruebaCaso 3: Preview en SwiftUI
struct MovieListView: View {
let presentation: Presentation
var body: some View {
List(presentation.movies) { movie in
Text(movie.title)
}
}
}
#Preview {
MovieListView(presentation: Presentation(repository: RepositoryTest()))
// Preview instantánea, sin esperar a ninguna API
}🎭 ¿Por qué esto es mejor que las librerías de inyección de dependencias?
Porque no necesitas aprender otra herramienta. No necesitas configurar nada. No dependes de código de terceros que puede quedar obsoleto o tener bugs. Es Swift puro. Son protocolos, que ya conoces (o deberías). Es inyección de dependencias en su forma más simple y efectiva. Además, el compilador te protege: si tu doble no implementa correctamente el protocolo, no compila. Seguridad en tiempo de compilación, no en ejecución.
💡 El verdadero poder: flexibilidad sin complejidad
Con este patrón puedes:
- Testear tu lógica sin tocar la red
- Ver previews de SwiftUI al instante
- Cambiar entre implementaciones según el entorno (debug/release)
- Crear mocks para diferentes escenarios (datos vacíos, errores, datos masivos)
- Mantener tu código desacoplado y fácil de mantener
Y todo sin añadir ni una sola dependencia externa. Solo aprovechando el poder de los protocolos y la inyección de dependencias, características nativas de Swift.
🔑 La clave: pensar en contratos, no en implementaciones
Cuando diseñes tu código, pregúntate: ¿qué DEBE hacer esto? No cómo lo hace, sino qué contrato debe cumplir. Define ese contrato en un protocolo. Luego crea las implementaciones que necesites. Tu código de negocio solo debe conocer el contrato. Esto es diseño orientado a interfaces y es la base de un código testable, mantenible y profesional.
