Para que los casos de XCTest pasen tanto individualmente como en conjunto, proporciona a cada prueba su propio fixture mutable e inyéctalo en el código que se está probando. Cuando un singleton deba seguir siendo compartido, restablécelo antes de cada prueba y evita accesos simultáneos durante toda su ejecución.

Esta guía sigue una tarea de desarrollo en Mac: corregir pruebas de cálculo de diseño en Xcode que pasan por separado, pero fallan al ejecutarse en conjunto. El ejemplo utiliza solo anchos enteros sintéticos y operaciones aritméticas locales. No requiere una ventana de la app, un mock de servicio, almacenamiento de archivos ni conexión de red.

¿Por qué una prueba puede pasar por separado y fallar en el conjunto?

Una prueba ejecutada de forma aislada puede encontrarse con el estado inicial de un singleton. En un conjunto, una prueba anterior quizá ya lo haya modificado. Una nueva referencia a ese singleton sigue apuntando al mismo objeto mutable.

Swift distingue entre propiedades de instancia y propiedades de tipo: cada instancia tiene sus propias propiedades de instancia, mientras que una propiedad de tipo tiene una única copia compartida. Las propiedades de tipo almacenadas se inicializan una vez, en el primer acceso. Por lo tanto, asignar SpacingCalculator.shared a una propiedad de prueba no crea una calculadora nueva. Consulta la documentación sobre propiedades del proyecto Swift.

Considera una fila sintética con elementos de anchos [10, 20, 30]. Su ancho total es la suma de los anchos de los elementos más los espacios entre ellos. Con un espacio predeterminado de 2, el resultado es 64; con un espacio de 8, es 76.

Supongamos que una prueba comprueba el ancho predeterminado sin configurar la calculadora, mientras que otra cambia el espacio compartido a 8. La tabla siguiente describe una ejecución secuencial en la que cada fila empieza con una calculadora nueva cuyo espacio es 2. Son cálculos ilustrativos, no resultados registrados de pruebas de Xcode.

Ejecución Calculadora compartida sin restablecer Calculadora nueva para cada prueba
Prueba predeterminada por separado 64: pasa 64: pasa
Prueba de espacio amplio por separado 76: pasa 76: pasa
Predeterminada y luego espacio amplio Ambas pasan Ambas pasan
Espacio amplio y luego predeterminada La prueba predeterminada recibe 76: falla Ambas pasan

Esto ofrece un diagnóstico útil: inspecciona qué prueba modifica el estado que utiliza la aserción fallida. Cambiar los nombres de las pruebas para obtener un orden favorable deja intacta la dependencia.

¿Qué método del ciclo de vida de XCTest debería encargarse del fixture?

Usa un método de configuración de instancia para el estado mutable que necesita cada prueba. Apple documenta que la configuración de instancia se ejecuta antes de cada método de prueba, mientras que la configuración a nivel de clase se ejecuta una vez antes de la clase de prueba. Por lo tanto, un fixture mutable creado en la configuración a nivel de clase no obtiene una base nueva entre métodos. Consulta Configurar y desmontar el estado de las pruebas.

Elige el método de configuración que corresponda al trabajo: la creación síncrona y no arrojable del fixture encaja en setUp(); la creación que puede arrojar errores encaja en setUpWithError(); la preparación asíncrona encaja en setUp() async throws. Usa el desmontaje para completar la limpieza de la prueba actual. Apple desaconseja explícitamente depender del desmontaje para preparar las pruebas siguientes, porque un fallo puede impedir que se ejecute. La misma documentación del ciclo de vida describe estos límites.

La elección del fixture depende tanto de quién es su propietario como del alcance de ejecución:

Situación Enfoque adecuado Condición que hay que comprobar
Las pruebas pueden crear la dependencia Crea e inyecta una instancia por prueba El consumidor debe utilizar esa instancia inyectada
Las pruebas deben usar el singleton real Establece una base completa antes de cada prueba Ninguna prueba simultánea puede modificar ese singleton
La configuración compartida es inmutable Reutiliza el valor inmutable Los objetos referenciados también deben permanecer sin cambios
Una prueba inicia trabajo asíncrono Espera a que termine antes de finalizar la prueba No debe quedar trabajo pendiente que modifique el fixture después

La primera opción es la más sencilla para esta calculadora. El código de producción puede conservar un valor predeterminado compartido, mientras que las pruebas eligen explícitamente instancias independientes.

Crea un fixture de XCTest de propiedad independiente

Usa un Mac con Xcode y un destino de pruebas unitarias para macOS existente. Añade el siguiente ejemplo completo como un archivo Swift en ese destino de pruebas. Define su propia calculadora, consumidor y pruebas, así que no necesita importar ningún módulo de la aplicación. Usa una cadena de herramientas Swift 6 con el aislamiento de actor predeterminado del destino configurado como nonisolated.

La calculadora es un actor para hacer explícito el límite de concurrencia de su estado mutable. Los actores de Swift restringen el acceso a su estado mutable, y las llamadas desde fuera del actor usan await. Estas reglas se documentan en el capítulo sobre concurrencia del proyecto Swift. La propiedad independiente del fixture sigue siendo necesaria incluso con un actor.

import XCTest

actor SpacingCalculator {
    static let shared = SpacingCalculator()

    private var gap = 2

    func setGap(_ value: Int) {
        precondition(value >= 0)
        gap = value
    }

    func width(of itemWidths: [Int]) -> Int {
        precondition(itemWidths.allSatisfy { $0 >= 0 })
        let gapCount = max(itemWidths.count - 1, 0)
        return itemWidths.reduce(0, +) + gapCount * gap
    }
}

struct RowLayout {
    let calculator: SpacingCalculator

    init(calculator: SpacingCalculator = .shared) {
        self.calculator = calculator
    }

    func width(of itemWidths: [Int]) async -> Int {
        await calculator.width(of: itemWidths)
    }
}

final class RowLayoutTests: XCTestCase {
    private var calculator: SpacingCalculator?

    override func setUp() {
        super.setUp()
        calculator = SpacingCalculator()
    }

    override func tearDown() {
        calculator = nil
        super.tearDown()
    }

    func testDefaultGap() async throws {
        let fixture = try XCTUnwrap(calculator)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [10, 20, 30])

        XCTAssertEqual(actual, 64)
    }

    func testWideGap() async throws {
        let fixture = try XCTUnwrap(calculator)
        await fixture.setGap(8)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [10, 20, 30])

        XCTAssertEqual(actual, 76)
    }

    func testEmptyRow() async throws {
        let fixture = try XCTUnwrap(calculator)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [])

        XCTAssertEqual(actual, 0)
    }

    func testSingleItem() async throws {
        let fixture = try XCTUnwrap(calculator)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [10])

        XCTAssertEqual(actual, 10)
    }
}

La línea decisiva es calculator = SpacingCalculator(). Cada configuración crea un actor independiente con un espacio de 2. Luego, cada prueba pasa ese actor a RowLayout. Llamar a RowLayout() dentro de estas pruebas seleccionaría .shared y evitaría usar el fixture.

El ejemplo permite intencionadamente la creación directa. Si tu singleton actual tiene un inicializador privado, introduce una vía de creación interna o extrae su estado de cálculo mutable a una dependencia que se pueda crear por separado. Se puede exponer un inicializador interno al destino de pruebas mediante el mecanismo establecido de importación con posibilidad de prueba del módulo de la aplicación; no hace falta que la referencia al singleton se pueda reemplazar globalmente.

La aritmética también define dos casos límite útiles: una fila vacía tiene un ancho de 0, y un elemento no tiene ningún espacio intermedio. El ejemplo acepta enteros no negativos lo bastante pequeños como para evitar el desbordamiento de enteros; los valores negativos infringen sus precondiciones y terminan la ejecución en lugar de producir un fallo de aserción.

Los cálculos aritméticos esperados y los dos órdenes de las pruebas se comprobaron localmente mediante cálculos deterministas en Python. Se inspeccionaron los ejemplos Swift, pero no se compilaron ni ejecutaron porque este entorno de investigación no dispone de una cadena de herramientas Swift. Ejecútalos en Xcode para verificar la compilación y la ejecución en tu destino.

¿Cuándo basta con restablecer el singleton?

Restablecerlo es una solución transitoria viable cuando las pruebas realmente necesitan la instancia compartida y se puede restaurar todo el estado visible durante las pruebas. Establece la base antes de cada prueba, incluidos los valores que una prueba presupone que son los predeterminados. Restablecer solo la propiedad modificada por la prueba que falla actualmente puede dejar sin descubrir otras dependencias.

Para esta calculadora, la configuración asíncrona podría seleccionar SpacingCalculator.shared y esperar a setGap(2) antes de cada método. Así se establece la base aritmética solo si ningún otro código que se ejecute simultáneamente cambia el espacio antes de la aserción. Si el fixture incorpora más adelante resultados en caché o configuración adicional, la operación de restablecimiento también deberá cubrir esos valores.

Un actor no concede a una prueba la propiedad exclusiva durante varias llamadas que esperan resultados. Por ejemplo, la prueba A puede establecer el espacio compartido en 2, la prueba B puede establecerlo en 8 y, luego, la prueba A puede calcular un ancho usando 8. Cada operación del actor puede ejecutarse correctamente aunque falle la suposición general de la prueba. Esto se desprende de las reglas documentadas de ejecución y suspensión de los actores de Swift.

Antes de elegir el aislamiento mediante restablecimiento, identifica todas las pruebas y tareas en segundo plano que puedan acceder al objeto compartido. Si no puedes garantizar su uso exclusivo durante la preparación, la ejecución y la limpieza, inyecta estado independiente. Restaurar una instantánea después no puede evitar que otro código observe los cambios temporales durante la prueba.

¿Cómo se traslada el fixture a Swift Testing?

Swift Testing inicializa una instancia de conjunto distinta para cada función de prueba de instancia. Por lo tanto, una propiedad de instancia que cree una calculadora proporciona el mismo patrón de propiedad sin redefinir los métodos de configuración de XCTest. Una propiedad inicializada desde .shared seguiría haciendo referencia al singleton. Apple documenta el ciclo de vida de las instancias en Organizar funciones de prueba con tipos de conjunto.

Con las definiciones anteriores de SpacingCalculator y RowLayout presentes una sola vez en el mismo destino de pruebas, lo siguiente constituye un conjunto adicional completo de Swift Testing. Requiere una cadena de herramientas que incluya el módulo Testing, como Xcode 16 o posterior. Su aislamiento de actor predeterminado también debería ser nonisolated.

import Testing

struct RowLayoutSwiftTests {
    let calculator = SpacingCalculator()

    @Test func defaultGap() async {
        let layout = RowLayout(calculator: calculator)
        let actual = await layout.width(of: [10, 20, 30])
        #expect(actual == 64)
    }

    @Test func wideGap() async {
        await calculator.setGap(8)
        let layout = RowLayout(calculator: calculator)
        let actual = await layout.width(of: [10, 20, 30])
        #expect(actual == 76)
    }
}

Para una preparación más compleja, Swift Testing admite inicializadores de conjunto, incluidos los asíncronos o que pueden arrojar errores. El artículo de Apple Migrar una prueba desde XCTest explica este reemplazo de los métodos de configuración de XCTest. Este fixture no necesita limpieza explícita porque solo posee estado de cálculo local y no inicia trabajo en segundo plano.

Swift Testing ejecuta las pruebas en paralelo de forma predeterminada. Aplicar .serialized a un conjunto ejecuta en serie las pruebas que contiene y los conjuntos anidados, pero no impide que se ejecuten al mismo tiempo pruebas no relacionadas. Aplicar este rasgo a una función de prueba ordinaria no parametrizada no tiene efecto. Estos límites se explicitan en Ejecutar pruebas en serie o en paralelo.

Por consiguiente, colocar dos conjuntos que dependen de un singleton bajo anotaciones .serialized separadas no establece exclusión mutua entre ellos. Un conjunto principal serializado común puede coordinar sus descendientes, siempre que todos los usuarios que compiten pertenezcan a ese ámbito. Los fixtures nuevos evitan esa necesidad de coordinación para estas pruebas de cálculo.

Verifica en Xcode los resultados individuales y del conjunto

Usa el mismo esquema, plan de pruebas y destino de Mac durante toda la verificación. Apple documenta cómo ejecutar métodos individuales, conjuntos y el plan de pruebas activo en Ejecutar pruebas e interpretar los resultados.

  1. Ejecuta individualmente cada uno de los cuatro métodos de XCTest mediante el control de reproducción del margen del editor. Se esperan 64, 76, 0 y 10, respectivamente.
  2. Ejecuta la clase RowLayoutTests mediante el control de reproducción junto a su declaración. Los cuatro métodos deberían pasar en conjunto.
  3. Elige Product > Test para ejecutar el plan de pruebas activo. Confirma que estas pruebas se ejecutaron realmente, en lugar de haberse omitido o excluido.
  4. Si añadiste el ejemplo de Swift Testing, ejecuta también sus dos funciones por separado y en conjunto.

Si las pruebas individuales pasan, pero falla el plan completo, sigue el objeto que utiliza el consumidor que falla. Busca una inyección omitida, un ayudante que llame a .shared o trabajo asíncrono que continúe después de que termine la prueba que lo inició. Si una prueba falla por separado, revisa sus entradas y el cálculo esperado antes de atribuir el fallo al orden del conjunto.

El fixture terminado debería hacer visible el estado inicial en la ruta de creación de la prueba actual. Para estos cálculos de diseño, cada prueba posee una calculadora, se la proporciona explícitamente al consumidor y espera a que termine su trabajo. Así, el resultado esperado no depende de cuál de las pruebas vecinas se ejecutó primero.

Fuentes