Programación en Swift y SwiftUI para iOS Developers

Swift Testing vs XCTest en Xcode

En el ecosistema de Apple, la evolución es la única constante. Durante más de una década, los iOS developer han confiado en XCTest como la herramienta estándar para garantizar la estabilidad de sus aplicaciones. Sin embargo, con la madurez de la programación Swift y el cambio de paradigma que trajo SwiftUI, se hacía evidente que necesitábamos una herramienta de pruebas que se sintiera menos heredada de Objective-C y más nativa.

Así nació Swift Testing.

Si estás desarrollando apps para iOS, macOS o watchOS en Xcode, te enfrentas a una encrucijada: ¿Sigo usando lo viejo conocido o abrazo lo nuevo? En este tutorial desglosaremos las diferencias, semejanzas y estrategias de migración entre Swift Testing y XCTest. Analizaremos el código, la filosofía y el rendimiento para que tomes la mejor decisión técnica para tu proyecto.


1. El Contexto: ¿Por qué dos Frameworks?

Para entender las diferencias, primero debemos entender el origen.

XCTest: El Veterano Confiable

XCTest ha sido el pilar de la calidad en Apple. Es un framework robusto, integrado profundamente en Xcode, que permite realizar pruebas unitarias, de integración, de interfaz de usuario (UI Tests) y de rendimiento. Sin embargo, su arquitectura basada en clases (XCTestCase) y su dependencia de la introspección del tiempo de ejecución (runtime) de Objective-C lo hacen sentir “pesado” en un mundo moderno de Swift. Las aserciones son verborrágicas y el manejo de la concurrencia moderna (async/await) fue un parche añadido posteriormente, no una característica nativa.

Swift Testing: El Aspirante Moderno

Presentado en la WWDC24, Swift Testing no es simplemente una “versión 2.0” de XCTest; es una reimaginación completa. Está diseñado específicamente para Swift. Utiliza el sistema de Macros (introducido en Swift 5.9) para generar código de prueba en tiempo de compilación, lo que lo hace más seguro, rápido y expresivo. Para un iOS developer que trabaja con Swift y SwiftUI, Swift Testing se siente natural: usa structs, maneja la concurrencia de forma nativa y tiene una sintaxis declarativa.


2. Semejanzas: Lo que No Cambia

Antes de ver las diferencias, es crucial entender qué tienen en común. Ambos frameworks comparten el mismo objetivo: asegurar que tu programación Swift funcione como se espera.

  1. Integración con Xcode: Ambos aparecen en el “Test Navigator” de Xcode. Los diamantes verdes y rojos (éxito/fallo) funcionan igual.
  2. Ejecución: Ambos se ejecutan con Cmd + U.
  3. Coexistencia: Puedes tener archivos de XCTest y archivos de Swift Testing en el mismo “Unit Testing Bundle”. Xcode es lo suficientemente inteligente para ejecutar ambos en la misma sesión.
  4. CI/CD: Ambos generan resultados que pueden ser interpretados por herramientas de Integración Continua (como Xcode Cloud, GitHub Actions o Jenkins) mediante archivos .xcresult.
  5. Scope: Ambos pueden testear lógica de negocio (Modelos, ViewModels en MVVM, Servicios) en plataformas iOS, macOS y watchOS.

3. Diferencias Clave: Filosofía y Arquitectura

Aquí es donde la carretera se bifurca. La diferencia fundamental radica en cómo cada framework ve el mundo.

Clases vs. Estructuras (Values vs. Reference Types)

XCTest obliga a usar Clases. Cada suite de pruebas debe heredar de XCTestCase.

// XCTest
import XCTest

final class CarritoTests: XCTestCase {
    var carrito: Carrito!
    
    override func setUp() {
        super.setUp()
        carrito = Carrito() // Reiniciar estado
    }
    
    func testAgregarItem() { ... }
}
  • El problema: Al ser clases (tipos de referencia), si olvidas reiniciar el estado en el tearDown o setUp, los cambios realizados en un test pueden “filtrarse” y afectar al siguiente test, creando “flaky tests” (tests intermitentes) difíciles de depurar.

Swift Testing promueve el uso de Estructuras (Structs) o Actors.

// Swift Testing
import Testing

struct CarritoTests {
    let carrito = Carrito()
    
    @Test func agregarItem() { ... }
}
  • La ventaja: Al usar struct (tipos de valor), Swift crea una copia nueva e inmutable de la suite para cada test. Esto garantiza el aislamiento total. No necesitas un método setUp para reiniciar variables simples; la inmutabilidad y la inicialización por defecto lo hacen por ti. Es “Swifty” en su máxima expresión.

4. Comparativa de Sintaxis: Aserciones vs. Expectativas

La legibilidad es vital en los tests. Los tests actúan como documentación viva de tu código.

XCTest: La era de la Verbosidad

En XCTest, tienes una función diferente para cada tipo de comprobación.

  • XCTAssertEqual(a, b)
  • XCTAssertTrue(a)
  • XCTAssertNil(a)
  • XCTAssertGreaterThan(a, b)

Si la aserción falla, XCTest te dice “X no es igual a Y”. A menudo, tienes que agregar un mensaje de String manual para entender el contexto.

func testSuma() {
    let resultado = 2 + 2
    XCTAssertEqual(resultado, 4, "La suma debería ser 4")
}

Swift Testing: El poder de las Macros

Swift Testing simplifica todo esto en dos macros principales: #expect y #require. Gracias a las macros, el framework puede “leer” tu código.

@Test func suma() {
    let resultado = 2 + 2
    #expect(resultado == 4)
}

Si este test falla, Swift Testing no solo te dice que falló. Expande la macro para mostrarte una visualización detallada de los valores:

Expectation failed: (resultado → 5) == 4

La diferencia crítica: #require En XCTest, si necesitas desempaquetar un opcional para continuar el test, solías hacer XCTUnwrap o un guard con XCTFail. En Swift Testing, try #require(valor) detiene la ejecución del test inmediatamente si falla, evitando crashes en líneas posteriores.


5. Tests Parametrizados: El gran salto

Aquí es donde Swift Testing humilla a XCTest. Como iOS developer, a menudo quieres probar la misma función con diferentes inputs (casos borde, nulos, strings vacíos).

En XCTest (La forma manual)

Tenías que hacer bucles dentro del test, lo cual es mala práctica porque si falla una iteración, el reporte no es claro sobre cuál fue, o se detiene todo el bucle.

// XCTest
func testValidarEmail() {
    let emails = ["test@test.com", "malo", "sin@dominio"]
    for email in emails {
        XCTAssertTrue(Validador.esValido(email)) 
        // Si "malo" falla, ¿sabes fácilmente cuál fue sin mirar logs?
    }
}

En Swift Testing (La forma nativa)

El framework soporta argumentos directamente en la macro @Test.

// Swift Testing
@Test("Validar formatos de email", arguments: [
    "test@test.com",
    "usuario.nombre@dominio.co.uk",
    "admin@localhost"
])
func validarEmail(email: String) {
    #expect(Validador.esValido(email))
}

Xcode generará una entrada de prueba individual para cada argumento. Si uno falla, los otros siguen ejecutándose y ves exactamente qué dato causó el error.


6. Concurrencia: Adiós a las Expectativas

El manejo de código asíncrono es esencial en apps modernas que usan SwiftUI y llamadas a red.

XCTest: XCTestExpectation

Antes de async/await, XCTest usaba expectativas, una API basada en callbacks y tiempos de espera. Incluso con async/await, XCTest a veces requiere trucos para asegurar que el MainActor se respete correctamente.

// XCTest
func testDescarga() {
    let expectation = XCTestExpectation(description: "Descarga datos")
    servicio.descargar { datos in
        XCTAssertNotNil(datos)
        expectation.fulfill()
    }
    wait(for: [expectation], timeout: 1.0)
}

Swift Testing: async/await Nativo

Swift Testing fue construido sobre la concurrencia estructurada de Swift.

// Swift Testing
@Test func descarga() async throws {
    let datos = try await servicio.descargar()
    #expect(datos != nil)
}

El framework maneja la espera automáticamente. Soporta funciones asyncthrows y el aislamiento de actores globales como @MainActor de forma transparente. Esto reduce drásticamente el código “boilerplate” y los errores humanos al configurar timeouts.


7. Organización y Filtrado: Tags vs Naming

Organizar tests en proyectos grandes es vital.

XCTest depende del esquema de nomenclatura. Si querías ejecutar solo los tests de “Login”, tenías que filtrar por el nombre de la función en el esquema de Xcode. No había forma programática de agrupar tests de diferentes clases bajo una categoría lógica como “SmokeTest” o “Network”.

Swift Testing introduce los Tags (Etiquetas). Puedes crear etiquetas personalizadas y aplicarlas a tests individuales o suites completas.

extension Tag {
    @Tag static var critico: Self
    @Tag static var red: Self
}

@Suite("Tests de Login")
@Tag(.critico)
struct LoginTests {
    @Test(.tags(.red))
    func loginRemoto() async { ... }
}

Luego, en Xcode, puedes configurar tu Test Plan para ejecutar solo tests con el tag .critico. Esto es un cambio de juego para pipelines de CI/CD eficientes.


8. Tutorial Práctico: Migrando de XCTest a Swift Testing

Vamos a ver un ejemplo “Side-by-Side” (Lado a lado) de un caso real. Supongamos que estamos testeando un ViewModelde una app en SwiftUI.

Escenario: ContadorViewModel

class ContadorViewModel: ObservableObject {
    @Published var cuenta = 0
    func incrementar() { cuenta += 1 }
    func reset() { cuenta = 0 }
}

Versión XCTest

import XCTest
@testable import MiApp

final class ContadorTests: XCTestCase {
    var vm: ContadorViewModel!
    
    override func setUp() {
        super.setUp()
        vm = ContadorViewModel()
    }
    
    override func tearDown() {
        vm = nil
        super.tearDown()
    }
    
    func testIncrementar() {
        // Given
        let valorInicial = vm.cuenta
        
        // When
        vm.incrementar()
        
        // Then
        XCTAssertEqual(vm.cuenta, valorInicial + 1)
    }
}

Versión Swift Testing

import Testing
@testable import MiApp

struct ContadorTests {
    let vm = ContadorViewModel()
    
    @Test("Verificar que el contador incrementa correctamente")
    func incrementar() {
        // Given
        let valorInicial = vm.cuenta
        
        // When
        vm.incrementar()
        
        // Then
        #expect(vm.cuenta == valorInicial + 1)
    }
}

Análisis de la migración:

  1. Eliminamos la herencia de XCTestCase.
  2. Cambiamos class por struct.
  3. Eliminamos setUp y tearDown; la inicialización de la propiedad let vm ocurre automáticamente para cada test.
  4. Reemplazamos func testIncrementar() por @Test func incrementar().
  5. Reemplazamos XCTAssertEqual por #expect.
  6. El código se redujo en un 30% y es más legible.

9. ¿Cuándo NO usar Swift Testing? (Limitaciones actuales)

Aunque Swift Testing es el futuro, XCTest todavía tiene su lugar en 2025/2026. Es importante que el ios developerconozca las limitaciones:

  1. UI Testing (XCUITest): Swift Testing está diseñado para pruebas unitarias y de integración lógica. Para automatizar la interacción con la UI (pulsar botones, scrollear, verificar elementos en pantalla), XCUITest (que es parte de XCTest) sigue siendo la única opción nativa de Apple. No puedes usar @Test para controlar XCUIApplication.
  2. Performance Testing: XCTest tiene measure { } para medir el rendimiento de un bloque de código y establecer líneas base. Swift Testing aún no tiene un equivalente directo con la misma profundidad de integración en Xcode para gráficos de rendimiento históricos (aunque esto podría cambiar en futuras actualizaciones de Xcode).
  3. Objective-C: Si tu proyecto es legacy y tiene pruebas escritas en Objective-C, Swift Testing no puede ejecutarlas ni interactuar directamente con ellas al mismo nivel que XCTest.

10. Estrategia para el Desarrollador iOS Moderno

Entonces, ¿qué deberías hacer hoy en tus proyectos de Swift y SwiftUI?

Proyectos Nuevos (Greenfield)

Usa Swift Testing por defecto para todas tus pruebas unitarias y de lógica de negocio. Es más rápido de escribir, más fácil de leer y aprovecha mejor las características modernas de Swift. Solo importa XCTest si necesitas hacer UI Tests o pruebas de rendimiento específicas.

Proyectos Existentes (Brownfield)

No reescribas todo tus tests de XCTest mañana.

  1. Adopción Incremental: Configura tu target de pruebas para soportar ambos frameworks (funciona “out of the box”).
  2. Nuevas Features: Escribe las pruebas para las nuevas funcionalidades usando Swift Testing.
  3. Refactorización: Cuando tengas que tocar una clase de test antigua para arreglar un bug o cambiar lógica, aprovecha para migrar esa clase específica a Swift Testing.

Conclusión

La introducción de Swift Testing marca un hito en la madurez de la programación Swift. Mientras que XCTest nos sirvió fielmente heredando la robustez del pasado, Swift Testing nos abre la puerta a un futuro más declarativo, seguro y concurrente, alineándose perfectamente con la filosofía de SwiftUI.

Las diferencias son claras: Swift Testing gana en sintaxis, seguridad de tipos (gracias a structs) y manejo de datos (tests parametrizados). XCTest retiene el trono únicamente en UI Testing y compatibilidad legacy.

Como iOS developer, dominar ambos frameworks te dará una ventaja competitiva. Entenderás no solo cómo escribir tests, sino por qué la arquitectura de tus pruebas impacta en la calidad final de tus aplicaciones en iOS, macOS y watchOS.

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

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

Qué es Swift Testing y cómo usarlo en Xcode

Next Article

Novedades de Xcode 26

Related Posts