Incluye la preparación asíncrona de fixtures compartidas en init() async throws de una suite de Swift Testing. Swift Testing completa ese inicializador antes de llamar a un método de prueba de instancia, por lo que el método recibe un estado listo para usar. En XCTest, usa setUp() async throws.

Esta guía está dirigida a quienes desarrollan para Mac y preparan datos de pruebas unitarias locales en Xcode. La tarea consiste en terminar la inicialización asíncrona antes de empezar con las aserciones, mediante cálculos deterministas con datos de entrada sintéticos. Apple documenta el enfoque con inicializadores en Migrating a test from XCTest.

¿Cómo espera Swift Testing a que se inicialicen las fixtures?

Para un método de prueba de instancia, Swift Testing inicializa el tipo de la suite contenedora y luego llama al método en esa instancia. La suite debe tener un inicializador que se pueda llamar sin argumentos; ese inicializador puede ser asíncrono, lanzar errores o ambas cosas. Cada método de prueba de instancia se ejecuta en una instancia distinta. Estas reglas del ciclo de vida están documentadas en Organizing test functions with suite types.

La dependencia resultante es sencilla: termina la preparación de las fixtures, finaliza el inicializador y comienza el método de prueba. El inicializador debe esperar realmente a que termine la operación de preparación. Iniciar el trabajo dentro del inicializador no establece por sí solo esa dependencia.

Usa la siguiente guía de decisión para elegir dónde debe hacerse la preparación:

Situación Dónde preparar ¿Qué garantiza que esté listo?
Varias pruebas de instancia necesitan la misma receta de fixture init() async throws de la suite El inicializador esperado termina antes de cada método
Solo una prueba necesita la fixture El cuerpo de una prueba async throws Un try await explícito antes de las aserciones
Una prueba existente hereda de XCTestCase setUp() async throws XCTest completa la configuración asíncrona antes del método
Una prueba de Swift Testing es static o global El cuerpo de la prueba o un helper invocado explícitamente La prueba espera explícitamente a que termine la preparación

Esta tabla aplica las reglas documentadas del ciclo de vida de los frameworks a la ubicación de las fixtures. En concreto, el inicializador de una suite es un punto de preparación por instancia; no significa que la preparación se haga una sola vez para toda la suite. Consulta las reglas de inicialización de suites y la secuencia de configuración de XCTest de Apple.

Añade un ejemplo completo de fixture asíncrona en Xcode

Usa Xcode 16 o posterior con la compatibilidad integrada con Swift Testing. El proyecto Swift documenta que Swift Testing se incluye con Swift 6 y Xcode 16, y que no hace falta añadir una dependencia de paquete. Consulta el README oficial de Swift Testing.

En un destino de pruebas unitarias existente, elige File > New > New File From Template y selecciona Swift Testing Unit Test. Reemplaza el contenido del archivo generado por el ejemplo completo siguiente. Un paquete de pruebas unitarias XCTest existente también puede incluir pruebas de Swift Testing; no hace falta un paquete separado. Estos pasos siguen la guía de Apple Adding tests to your Xcode project.

import Testing

enum FixtureError: Error {
    case invalidInput
}

private actor SquareCalculator {
    let values: [Int]

    init(values: [Int]) {
        self.values = values
    }

    func squares() -> [Int] {
        values.map { $0 * $0 }
    }
}

struct CalculationFixture {
    let squares: [Int]
    let total: Int

    static func make(values: [Int]) async throws -> Self {
        guard !values.isEmpty,
              values.count <= 100,
              values.allSatisfy({ (0...100).contains($0) }) else {
            throw FixtureError.invalidInput
        }

        let calculator = SquareCalculator(values: values)
        let squares = await calculator.squares()

        return Self(
            squares: squares,
            total: squares.reduce(0, +)
        )
    }
}

@Suite
struct CalculationTests {
    let fixture: CalculationFixture

    init() async throws {
        fixture = try await CalculationFixture.make(values: [2, 4, 6])
    }

    @Test
    func squaresAreReady() {
        #expect(fixture.squares == [4, 16, 36])
    }

    @Test
    func totalIsReady() {
        #expect(fixture.total == 56)
    }
}

El ejemplo usa un actor local real para realizar el cálculo. Llamar a su método aislado desde fuera del actor requiere await. No hay demoras, datos de entrada aleatorios, dependencias externas ni operaciones en segundo plano que deban terminar justo a tiempo. El resultado sigue siendo determinista, tanto si la ejecución llega a suspenderse como si no.

La fábrica de fixtures rechaza entradas vacías, más de 100 valores y valores fuera de 0...100. Estos límites ilustrativos mantienen pequeño el cálculo: cada cuadrado es como máximo 10 000 y el total como máximo 1 000 000. Los valores no válidos generan un error en vez de eliminarse o reemplazarse silenciosamente.

Para la entrada proporcionada, el cálculo esperado es 2² + 4² + 6² = 4 + 16 + 36 = 56. El inicializador guarda la fixture completada en una propiedad let no opcional. Por eso, los métodos de prueba no necesitan una marca de disponibilidad ni desenvolver opcionales.

Los métodos pueden seguir siendo síncronos porque su requisito asíncrono previo ya se completó en el inicializador. Si el trabajo asíncrono debe realizarse dentro del cuerpo de una prueba, el patrón documentado por Apple consiste en marcar esa función como async y esperar directamente a la operación. Consulta Testing asynchronous code.

El cálculo sintético se comprobó localmente con Python. Swift no está disponible en este entorno de investigación, así que se inspeccionaron los ejemplos en Swift, pero no se compilaron ni ejecutaron aquí. Los resultados esperados anteriores describen el cálculo del ejemplo, no un resultado de ejecución de pruebas en Xcode.

¿Por qué iniciar una tarea no significa que la configuración haya terminado?

Un error frecuente es iniciar Task { ... } desde un inicializador síncrono y volver de inmediato. En ese caso, el inicializador termina de iniciar el trabajo, pero este todavía puede estar ejecutándose cuando comienza el método de prueba.

La guía del lenguaje Swift describe Task { ... } como una forma de crear una tarea no estructurada. El identificador que devuelve se puede usar para esperar el resultado mediante await handle.value. Descartar el identificador elimina ese punto explícito de espera. Consulta la sección Unstructured Concurrency del libro oficial de Swift.

Para inicializar una fixture, esperar directamente a la fábrica en un inicializador asíncrono suele expresar la dependencia con mayor claridad. Si un helper existente inicia trabajo internamente y devuelve el control antes de que termine, esperar al helper solo espera a que este retorne. Cambia su contrato de finalización para que, cuando retorne, la fixture esté lista, o expón un resultado de tarea que quien lo llama pueda esperar.

Añadir una pausa fija no expresa esa dependencia. Solo deja pasar el tiempo, por lo que la disponibilidad depende de que la preparación termine dentro de ese intervalo. Del mismo modo, marcar el método de prueba como async solo permite las llamadas asíncronas; el método aún debe esperar a que termine la operación de la que depende.

¿Cuál es la configuración equivalente en XCTest?

XCTest proporciona un punto de enganche específico para la configuración asíncrona. El orden documentado antes de cada método de prueba es setUp() async throws, luego setUpWithError() y, después, setUp() síncrono. A continuación se ejecuta el método de prueba. Consulta Set Up and Tear Down State in Your Tests.

Para comparar la misma fixture con XCTest, añade el siguiente archivo complementario completo al mismo destino de pruebas unitarias que el primer ejemplo. Importa XCTest y usa CalculationFixture, definida anteriormente y visible dentro del módulo. Conserva las definiciones auxiliares del primer archivo; no las dupliques.

import XCTest

final class CalculationXCTests: XCTestCase {
    private var fixture: CalculationFixture?

    override func setUp() async throws {
        try await super.setUp()
        fixture = try await CalculationFixture.make(values: [2, 4, 6])
    }

    func testTotalIsReady() throws {
        let readyFixture = try XCTUnwrap(fixture)
        XCTAssertEqual(readyFixture.squares, [4, 16, 36])
        XCTAssertEqual(readyFixture.total, 56)
    }
}

El método de XCTest también puede ser síncrono: la preparación asíncrona se realiza durante la configuración. La propiedad opcional se adapta a la fase de configuración independiente de XCTest, y XCTUnwrap convierte la ausencia de una fixture en un error explícito. Apple documenta ese patrón para desenvolver valores en su guía de migración.

Al convertir esta prueba a Swift Testing, traslada la preparación al inicializador de la suite y usa import Testing, @Test y las expectativas de Swift Testing. En una suite ordinaria de Swift Testing, un método llamado simplemente setUp no es el mecanismo de configuración documentado; la ruta de migración de Apple sustituye los métodos de configuración por la inicialización.

Comprueba estos límites si la inicialización todavía parece adelantarse

Primero, comprueba si la prueba es un método de instancia. Una prueba static no necesita una instancia de la suite, así que no puede depender de la inicialización automática de instancias. Una prueba global tampoco tiene una instancia contenedora. En cualquiera de esos casos, crea y espera la fixture en el cuerpo de la prueba. Los formatos de prueba admitidos se describen en Migrating a test from XCTest.

Segundo, revisa toda la cadena de llamadas de preparación. Encuentra la operación que realmente produce el estado listo y asegúrate de que cada invocador espere a que termine. En el ejemplo, el cálculo del actor termina antes de que retorne la fábrica, y la fábrica retorna antes de que termine el inicializador. Ningún trabajo que necesiten las aserciones queda fuera de esa secuencia.

Tercero, permite que se propaguen los errores de configuración. Si CalculationFixture.make lanza un error, la instancia de la suite no puede terminar de inicializarse, así que su método de prueba de instancia no puede continuar en esa instancia. Evita sustituir la preparación fallida por una fixture vacía mediante try? y un valor alternativo, salvo que ese comportamiento sea precisamente lo que se está probando. El error por datos de entrada no válidos del ejemplo impide deliberadamente que se cree una fixture que parezca utilizable.

Por último, distingue el orden de inicialización de la planificación de pruebas. De forma predeterminada, Swift Testing ejecuta las pruebas en paralelo. El rasgo .serialized de una suite puede hacer que las pruebas que contiene se ejecuten en serie, pero la serialización no convierte una tarea que no se esperaba en una preparación esperada, ni hace que el inicializador de la suite se ejecute una sola vez. Apple documenta el alcance del rasgo en Running tests serially or in parallel.

Para este cálculo local, mantén la fixture inmutable y prepárala por separado para cada prueba de instancia. La comprobación esencial es la cadena de dependencias: la operación de preparación devuelve una fixture terminada, el inicializador espera ese resultado y la prueba lee la propiedad inicializada. Así, la prueba tiene una condición concreta de disponibilidad sin depender de supuestos sobre el tiempo.

Fuentes