En la era de UIKit, probar la interfaz de usuario (UI) era una tarea que muchos iOS developers evitaban. Los UI Tests eran lentos, frágiles y difíciles de mantener. Sin embargo, con la llegada de la programación Swift moderna y el paradigma declarativo, las reglas han cambiado.
Hacer tests a vistas en SwiftUI no es solo posible, es esencial para garantizar la estabilidad de aplicaciones en iOS, macOS y watchOS. A diferencia de los ViewControllers monolíticos del pasado, las vistas de SwiftUI son ligeras y dependen del estado. Esto nos abre dos caminos: testear la lógica (Unit Testing) y testear la integración visual (UI Testing).
En este tutorial aprenderás a dominar ambos mundos utilizando Xcode. Transformaremos tu flujo de trabajo para que dejes de “probar con el dedo” y empieces a automatizar la calidad de tu software.
El Dilema de SwiftUI: ¿Qué estamos probando realmente?
Antes de escribir código, debemos entender la arquitectura. En SwiftUI, la vista es una función del estado (`View = f(State)`). Dado que las vistas son estructuras (`structs`) de valor y no objetos de referencia, no puedes simplemente instanciar una vista en un test unitario y comprobar si una etiqueta tiene cierto texto, porque la vista no se “renderiza” en los tests unitarios tradicionales.
Por lo tanto, la estrategia para un iOS developer senior se divide en dos:
- Unit Testing del ViewModel: Verificas que la lógica de negocio cambia el estado correctamente.
- UI Testing (XCUITest): Verificas que, dado un estado, la pantalla muestra los elementos correctos e interactúa bien.
Parte 1: Preparando la Vista para el Testeo
El secreto para unos tests de UI robustos en Swift no está en el código del test, sino en el código de la vista. Necesitamos una forma de identificar los elementos en la pantalla que sea resistente a cambios de diseño o idioma. Para esto, utilizamos accessibilityIdentifier.
Imaginemos una pantalla de Login simple. Añadiremos identificadores que XCUITest podrá encontrar.
import SwiftUI
struct LoginView: View {
@StateObject private var viewModel = LoginViewModel()
var body: some View {
VStack(spacing: 20) {
TextField("Usuario", text: $viewModel.username)
.accessibilityIdentifier("usernameField") // Clave para el test
.textFieldStyle(.roundedBorder)
SecureField("Contraseña", text: $viewModel.password)
.accessibilityIdentifier("passwordField")
.textFieldStyle(.roundedBorder)
if let error = viewModel.errorMessage {
Text(error)
.foregroundColor(.red)
.accessibilityIdentifier("errorText")
}
Button("Entrar") {
viewModel.login()
}
.accessibilityIdentifier("loginButton")
.disabled(viewModel.isLoading)
}
.padding()
}
}
Al agregar .accessibilityIdentifier("string"), creamos un gancho invisible para el usuario pero visible para el robot de pruebas de Xcode.
Parte 2: UI Testing con XCUITest
XCUITest es el framework nativo de Apple. Funciona como una “caja negra”: la aplicación se lanza de verdad y el test interactúa con ella como si fuera un usuario humano.
Configuración del Test Case
Crea un nuevo target de “UI Testing Bundle” en Xcode si no lo tienes. Luego, crea un archivo de prueba. Lo primero es asegurar que la app se lanza limpia.
import XCTest
final class LoginUITests: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
// En UI Tests, es vital manejar los fallos inmediatamente
continueAfterFailure = false
app = XCUIApplication()
app.launchArguments = ["-isRunningUITests"] // Truco para mockear datos
app.launch()
}
override func tearDownWithError() throws {
app = nil
}
}
Escribiendo tu primer Test de Interacción
Ahora vamos a escribir un test que escriba en los campos y pulse el botón. Aquí es donde los identificadores brillan para hacer tests a vistas en swiftui.
func testLoginSuccess() throws {
// 1. Localizar elementos
let usernameField = app.textFields["usernameField"]
let passwordField = app.secureTextFields["passwordField"]
let loginButton = app.buttons["loginButton"]
// 2. Verificar existencia (Sanity Check)
XCTAssertTrue(usernameField.exists, "El campo de usuario debería existir")
XCTAssertTrue(loginButton.exists, "El botón de login debería existir")
// 3. Interactuar
usernameField.tap()
usernameField.typeText("iOSDeveloper")
passwordField.tap()
passwordField.typeText("SwiftUI123")
loginButton.tap()
// 4. Aseverar el resultado (Esperamos navegar a Home)
let welcomeMessage = app.staticTexts["welcomeText"]
// IMPORTANTE: En UI Tests, las cosas tardan en aparecer.
// Usamos waitForExistence
XCTAssertTrue(welcomeMessage.waitForExistence(timeout: 5.0), "Deberíamos ver el mensaje de bienvenida tras el login")
}
Nota Pro: El uso de waitForExistence(timeout:) es crucial. Las animaciones de SwiftUI toman tiempo. Si usas XCTAssertTrue(welcomeMessage.exists) inmediatamente después del tap, el test fallará porque la animación no ha terminado.
Parte 3: El Patrón “Page Object”
Si escribes muchos tests así, tu código se volverá inmanejable. Si cambias un identificador, tendrás que arreglar 20 tests. Para evitar esto, los expertos en programación Swift usan el patrón Page Object.
Creamos una clase auxiliar que representa la pantalla.
import XCTest
class LoginPage {
let app: XCUIApplication
init(app: XCUIApplication) {
self.app = app
}
// Elementos
var usernameField: XCUIElement { app.textFields["usernameField"] }
var passwordField: XCUIElement { app.secureTextFields["passwordField"] }
var loginButton: XCUIElement { app.buttons["loginButton"] }
// Acciones
@discardableResult
func login(user: String, pass: String) -> Self {
usernameField.tap()
usernameField.typeText(user)
passwordField.tap()
passwordField.typeText(pass)
loginButton.tap()
return self
}
}
Ahora tu test es legible y elegante:
func testLoginWithPageObject() {
let loginPage = LoginPage(app: app)
loginPage.login(user: "User", pass: "Pass")
XCTAssertTrue(app.staticTexts["welcomeText"].waitForExistence(timeout: 5))
}
Parte 4: Unit Testing de la Lógica (ViewModel)
Aunque el artículo se centra en vistas, no puedes ignorar el motor. SwiftUI fomenta MVVM. Testear el ViewModel es más rápido (milisegundos vs segundos) que los UI Tests.
Supongamos que tu vista muestra un error si el password es corto. No necesitas lanzar el simulador para probar eso.
import XCTest
@testable import MiAppSwiftUI
final class LoginViewModelTests: XCTestCase {
func testPasswordValidationTooShort() {
// Given
let viewModel = LoginViewModel()
// When
viewModel.username = "Dev"
viewModel.password = "123" // Muy corta
viewModel.login()
// Then
XCTAssertNotNil(viewModel.errorMessage)
XCTAssertEqual(viewModel.errorMessage, "La contraseña es muy corta")
}
}
Si este test pasa, y tienes un UI Test simple que verifica que “Si `errorMessage` no es nil, aparece un Texto rojo”, entonces has cubierto el ciclo completo sin redundancia excesiva.
Parte 5: Mocking de Datos para Vistas
Uno de los mayores desafíos al hacer tests a vistas en swiftui es la red. No quieres que tus tests fallen porque no hay internet. Debes “engañar” a la app.
Utilizamos launchArguments en Xcode. Cuando la app arranca, comprueba si tiene ese argumento y carga un servicio de datos falso (Mock).
En tu código de App (Producción):
class DataServiceFactory {
static func create() -> DataProtocol {
if ProcessInfo.processInfo.arguments.contains("-isRunningUITests") {
return MockDataService() // Devuelve datos fijos instantáneos
} else {
return RealNetworkService()
}
}
}
Parte 6: Tests de Accesibilidad y Renderizado
Para un iOS developer, la accesibilidad no es opcional. XCUITest te permite auditar esto automáticamente.
func testAccessibility() {
// Escanea la jerarquía de vistas actual en busca de problemas
// como botones sin etiquetas o contraste bajo.
try? app.performAccessibilityAudit()
}
Snapshot Testing (Mención Honorífica)
A veces quieres asegurar que ningún píxel ha cambiado. Aunque XCUITest no hace esto nativamente bien, librerías como SnapshotTesting (de Point-Free) son estándares en la industria. Renderizan la vista de SwiftUI en una imagen y la comparan con una referencia guardada en el disco.
Consideraciones para macOS y watchOS
SwiftUI es multiplataforma, y tus tests también.
- macOS: Los interacciones son diferentes. No hay “tap”, hay “click”. XCUITest maneja esto, pero asegúrate de usar
.click()en lugar de.tap()si compartes código de tests, o crea extensiones que abstraigan la acción. - watchOS: El espacio es limitado. Los tests de UI en el reloj son más lentos. Aquí es vital priorizar los Unit Tests del ViewModel sobre los UI Tests extensivos.
Trucos Avanzados en Xcode
- Grabar Tests: En la parte inferior del editor de código en un archivo de UI Test, hay un botón rojo (Record). Si lo pulsas y usas el simulador, Xcode escribirá el código Swift por ti. Es genial para empezar, aunque el código generado suele necesitar limpieza.
- Esperas asíncronas: A veces
waitForExistenceno es suficiente. Puedes usarXCTNSPredicateExpectationpara esperar condiciones complejas, como que un botón se habilite después de una validación de red.
Conclusión
Ya no buscamos acceder a las propiedades internas de la vista (label.text), sino que probamos el comportamiento externo (lo que ve el usuario) mediante XCUITest y el comportamiento interno (estado) mediante Unit Tests.
Como iOS developer, integrar estos tests en tu flujo de trabajo en Xcode te dará una confianza inquebrantable para refactorizar y mejorar tu aplicación. No esperes a que un usuario reporte un fallo; encuéntralo tú antes con un test.
Si tienes cualquier duda sobre este artículo, contacta conmigo y estaré encantado de ayudarte 🙂. Puedes contactar conmigo en mi perfil de X o en mi perfil de Instagram










