XCTest lleva siendo el framework de tests de Apple desde hace más de una década. Funciona, pero su diseño arrastra decisiones de la era Objective-C que resultan incómodas en Swift moderno: la herencia obligatoria de XCTestCase, los métodos que empiezan por test por convención, y una API de aserciones que no aprovecha las características del lenguaje. Con Xcode 16, Apple presentó Swift Testing, un framework nuevo diseñado desde cero para Swift que cambia bastante la forma de escribir tests.
Qué es Swift Testing
Swift Testing es un framework open source (disponible en github.com/swiftlang/swift-testing) que Apple integró en Xcode 16 y que funciona también con Swift Package Manager en Linux. No reemplaza a XCTest en el sentido de que ambos pueden coexistir en el mismo proyecto, pero la intención es que Swift Testing sea la opción por defecto para tests nuevos.
Sus características principales:
- Tests definidos con la macro
@Testen lugar de herencia de clase. - Aserciones con
#expecty#requireque muestran los valores reales en los errores. - Parametrización nativa de tests.
- Organización con
Suitey tags. - Soporte completo para
async/await.
Tu primer test con Swift Testing
No hace falta subclasificar nada. Un test es simplemente una función anotada con @Test:
import Testing
@Test("El carrito calcula el total correctamente")
func totalCarrito() {
let carrito = Carrito()
carrito.agregar(Producto(precio: 10.0))
carrito.agregar(Producto(precio: 5.5))
#expect(carrito.total == 15.5)
}
La diferencia con XCTest se ve enseguida cuando el test falla. #expect muestra automáticamente los valores de ambos lados de la expresión, sin necesidad de mensaje manual:
// Si carrito.total fuera 14.5: // Expectation failed: (carrito.total ? 14.5) == 15.5
Parametrización de tests
Una de las características más útiles es la parametrización nativa. En XCTest tenías que escribir un bucle o múltiples funciones. Con Swift Testing:
@Test("Validar emails", arguments: [
("[email protected]", true),
("sin-arroba.com", false),
("@dominio.com", false),
("[email protected]", true)
])
func validarEmail(email: String, esValido: Bool) {
#expect(Validador.esEmailValido(email) == esValido)
}
Xcode muestra cada combinación como un test independiente en el panel de resultados, con su propio estado de éxito o fallo. Si un caso falla, puedes relanzarlo individualmente.
Suites para organizar los tests
En lugar de clases que heredan de XCTestCase, Swift Testing usa structs o clases anotadas con @Suite:
@Suite("Tests del servicio de autenticación")
struct AuthServiceTests {
let servicio = AuthService()
@Test("Login correcto devuelve token")
func loginCorrecto() async throws {
let token = try await servicio.login(usuario: "admin", contraseña: "1234")
#expect(!token.isEmpty)
}
@Test("Login incorrecto lanza error")
func loginIncorrecto() async throws {
await #expect(throws: AuthError.credencialesInvalidas) {
try await servicio.login(usuario: "admin", contraseña: "mal")
}
}
}
Usar structs en lugar de clases tiene una ventaja: cada test obtiene una copia fresca del struct, eliminando el estado compartido entre tests de forma automática.
require vs expect
#expect registra el fallo pero continúa ejecutando el test. #require lanza una excepción y detiene el test en ese punto, útil cuando los pasos siguientes dependen del resultado anterior:
@Test("Procesar respuesta de API")
func procesarRespuesta() throws {
let datos = cargarDatosMock()
let respuesta = try #require(parsear(datos))
// Si parsear devuelve nil, el test se detiene aquí
#expect(respuesta.estado == "ok")
#expect(respuesta.items.count > 0)
}
Tags para filtrar tests
Los tags permiten agrupar tests de distintas suites y filtrarlos en el panel de Xcode o al ejecutar desde línea de comandos:
extension Tag {
@Tag static var red: Self // tests que requieren red
@Tag static var lento: Self // tests de integración lentos
}
@Test("Sincronizar con servidor", .tags(.red, .lento))
func sincronizar() async throws {
// ...
}
Al ejecutar los tests, puedes incluir o excluir tags específicos, lo que resulta práctico en CI para separar tests unitarios rápidos de tests de integración.
Convivencia con XCTest
Swift Testing y XCTest pueden coexistir en el mismo target. No es necesario migrar todos los tests existentes de golpe. La regla práctica: usa Swift Testing para tests nuevos, mantén XCTest donde ya funciona, y migra gradualmente cuando toques un fichero de tests por otro motivo.
Lo que no puedes mezclar dentro del mismo fichero es @Test con herencia de XCTestCase. Son mundos separados, pero perfectamente compatibles en el mismo proyecto y en el mismo esquema de test.
Swift Testing en Linux y Swift Package Manager
A diferencia de XCTest, Swift Testing está disponible en Linux sin necesidad de herramientas adicionales. Si tu proyecto usa Swift Package Manager y tiene una librería que corre en servidor, puedes usar Swift Testing directamente:
// Package.swift
.testTarget(
name: "MiLibreriaTests",
dependencies: [
"MiLibreria",
.product(name: "Testing", package: "swift-testing")
]
)
Esto es especialmente relevante para proyectos como los que usan concurrencia en Go o frameworks de servidor Swift como Vapor, donde los tests en Linux son parte del pipeline de CI habitual.
Imagen: Pexels / Daniil Komov
