Al migrar pruebas unitarias de XCTest en un Mac, conserva la secuencia de propiedad: termina el trabajo, libera las referencias fuertes previstas y, después, comprueba la referencia débil. Para la limpieza síncrona, un defer local a la prueba hace explícita esa secuencia.
Este recorrido se centra en convertir una prueba unitaria sensible a la propiedad en Xcode. El ejemplo calcula el número de celdas de una cuadrícula sintética y, después, comprueba que se libere su calculadora. Solo usa objetos Swift locales y operaciones aritméticas con enteros.
¿Qué comportamiento del ciclo de vida debe conservar la migración?
XCTest proporciona una secuencia gestionada por el framework. Cuando termina un método de prueba, ejecuta los bloques de desmontaje registrados en orden inverso al registro y, a continuación, los métodos de desmontaje de la instancia. La limpieza registrada también se ejecuta si falla una aserción de prueba; la terminación del proceso puede impedirlo. Estos detalles importan cuando un bloque libera un objeto y otro comprueba su referencia débil. Configurar y desmontar el estado en las pruebas
Por ejemplo, un bloque de desmontaje que comprueba que una referencia sea nil debe ejecutarse después del bloque que elimina al último propietario previsto. Como XCTest usa el orden inverso al registro, primero se registra la comprobación y luego la liberación. Invertir esos registros cambia lo que observa la prueba.
La guía de migración de Swift Testing asigna la configuración a un inicializador y el desmontaje a un desinicializador de una suite de clase o actor. Esa correspondencia es útil para la limpieza asociada al ciclo de vida de la suite. Una aserción de propiedad requiere una decisión adicional: ¿qué referencia debe desaparecer exactamente antes de ejecutar la aserción? Migrar una prueba de XCTest
Usa esta tabla de decisiones antes de cambiar la sintaxis de la prueba:
| Responsabilidad existente | Opción de migración | Motivo |
|---|---|---|
| Crear un estado de fixture nuevo para cada prueba de instancia | Inicialización de una propiedad almacenada o init() |
Establece el fixture antes de ejecutar la prueba |
| Liberar un objeto e inspeccionar inmediatamente una referencia débil | Liberación explícita seguida de #expect, normalmente dentro de un bloque defer local |
Hace visible el punto de observación |
| Realizar limpieza síncrona asociada al ciclo de vida de una suite de tipo referencia | Suite de clase o actor con deinit |
Sigue la correspondencia documentada del ciclo de vida |
| Esperar la limpieza o informar de un error de limpieza | Limpieza esperada en una función asíncrona o un ámbito personalizado apropiado | Requiere una ruta de ejecución que admita la suspensión y el manejo de errores |
La tabla es una ayuda para decidir la migración, derivada de las reglas documentadas del ciclo de vida y del lenguaje. No implica que todos los hooks de XCTest tengan un método equivalente en Swift Testing.
¿Por qué mover la comprobación débil a deinit puede cambiar la prueba?
Swift llama al desinicializador de una clase justo antes de desasignar la instancia. El desinicializador todavía puede acceder a las propiedades almacenadas de esa instancia. Por lo tanto, entrar en deinit de una suite no demuestra por sí solo que ya se haya liberado un objeto retenido por una de esas propiedades. Desinicialización
Considera una suite con una propiedad fuerte layout. Una referencia débil a esa disposición puede seguir siendo distinta de nil mientras se ejecuta el desinicializador de la suite. La prueba debe eliminar la referencia fuerte de la propiedad antes de comprobar si se liberó la disposición.
Hay otro modo de fallo: un ciclo de referencias que retenga la suite puede impedir que se ejecute su desinicializador. Una aserción colocada solo en ese desinicializador no tendrá entonces la oportunidad de informar de la retención. Esto se deriva de las reglas de propiedad de ARC: las referencias fuertes mantienen vivas las instancias, mientras que las referencias débiles no impiden su desasignación y pasan a ser nil cuando se desasigna la instancia referenciada. Conteo automático de referencias
En una prueba de propiedad, mantén el observador fuera del objeto cuya liberación se está comprobando. Proporciona a la prueba una operación de liberación concreta y ejecuta su aserción mediante el flujo de control normal de la prueba. Así, el contrato de la prueba se puede inspeccionar sin depender de que se desasigne la propia suite.
Establecer la prueba de referencia de XCTest en Xcode
Usa un destino de pruebas unitarias existente en Xcode 16 o posterior. Apple documenta que XCTest y Swift Testing pueden coexistir en el mismo destino y paquete, por lo que la migración no requiere otro paquete de pruebas unitarias. Para añadir un archivo de prueba, usa File > New > New File From Template y selecciona XCTest Unit Test o Swift Testing Unit Test, según corresponda. Añadir pruebas al proyecto de Xcode
El siguiente es un archivo de prueba XCTest completo. Su calculadora devuelve 3 * 4, por lo que el resultado esperado es 12. Los dos registros de desmontaje exponen deliberadamente el orden de ejecución requerido.
import XCTest
private final class GridCalculator {
func cellCount(columns: Int, rows: Int) -> Int {
columns * rows
}
}
final class GridCalculatorXCTests: XCTestCase {
private var calculator: GridCalculator?
override func setUp() {
super.setUp()
calculator = GridCalculator()
}
func testCellCountAndRelease() {
weak var observed = calculator
// Registered first, so this executes second.
addTeardownBlock { [weak observed] in
XCTAssertNil(observed)
}
// Registered second, so this executes first.
addTeardownBlock { [self] in
calculator = nil
}
XCTAssertEqual(
calculator?.cellCount(columns: 3, rows: 4),
12
)
}
}
El bloque de liberación retiene el caso de prueba para poder actualizar la propiedad del fixture. El bloque de observación captura la calculadora de forma débil, lo que evita añadir otro propietario fuerte en la propia comprobación. Apple documenta que los bloques de desmontaje sirven para la limpieza específica de cada prueba, registrada junto con la configuración asociada. Configurar y desmontar el estado en las pruebas
El contrato de referencia está definido de forma precisa: cuando termina el cuerpo de la prueba y se borra la propiedad del fixture, la calculadora ya no debe existir. No mide el consumo de memoria ni demuestra que se haya liberado cada objeto creado por la aplicación.
Migrar la secuencia de propiedad a un ámbito local
Para este caso síncrono, usa una suite struct y coloca la liberación y la verificación en un único bloque defer. Swift ejecuta el código diferido cuando el control sale de su ámbito, incluso si hay un retorno anticipado o se lanza un error. Varios bloques diferidos se ejecutan en orden inverso; un solo bloque facilita la lectura de la secuencia de este ejemplo, que libera antes de comprobar. Flujo de control
El siguiente es un archivo independiente y completo de Swift Testing. Sustituye con él el archivo de referencia o conserva los ejemplos en archivos separados para que sus tipos auxiliares privados permanezcan separados. La segunda prueba es un control de diagnóstico: demuestra que un propietario fuerte adicional mantiene poblada la referencia débil hasta que se elimina ese propietario.
import Testing
private final class GridCalculator {
func cellCount(columns: Int, rows: Int) -> Int {
columns * rows
}
}
private final class GridFixture {
var calculator: GridCalculator? = GridCalculator()
var additionalOwner: GridCalculator?
}
struct GridCalculatorTests {
@Test
func cellCountAndRelease() throws {
let fixture = GridFixture()
weak var observed = fixture.calculator
defer {
fixture.calculator = nil
#expect(observed == nil)
}
let result = try #require(
fixture.calculator?.cellCount(columns: 3, rows: 4)
)
#expect(result == 12)
}
@Test
func weakProbeDetectsAnAdditionalOwner() {
let fixture = GridFixture()
weak var observed = fixture.calculator
fixture.additionalOwner = fixture.calculator
fixture.calculator = nil
#expect(observed != nil)
fixture.additionalOwner = nil
#expect(observed == nil)
}
}
En la primera prueba, el fixture es propietario de la calculadora y observed la observa sin ser su propietario. El cálculo produce un entero opcional, que #require desenvuelve. No se almacena una calculadora desenvuelta en otra variable local. Al salir del ámbito, el bloque diferido borra la referencia del fixture y comprueba el observador.
Un #expect fallido registra un problema y permite que la ejecución continúe. Un #require fallido lanza un error y detiene esa ruta de ejecución. Como el bloque defer se registra antes del requisito, su limpieza y comprobación débil se ejecutan igualmente cuando el requisito lanza un error. Expectativas y confirmaciones, Manejo de errores
El control de diagnóstico establece dos estados esperados distintos: borrar uno de los dos propietarios deja el observador distinto de nil; borrar el propietario restante lo deja en nil. Son comprobaciones de propiedad ilustrativas, no resultados medidos de la aplicación. Los ejemplos de Swift se inspeccionaron, pero no se pudieron compilar en el entorno de investigación porque no había disponible un ejecutable de Swift.
Comprobar los límites de los helpers, la concurrencia y la limpieza asíncrona
Un defer local pertenece a su ámbito contenedor. Si se mueve el bloque diferido a un helper de fábrica, se ejecutará cuando ese helper retorne, quizá antes de que la prueba use el objeto devuelto. Mantén el bloque en la prueba o diseña un helper que ejecute toda la operación dentro de su ámbito. El ámbito contenedor forma parte del contrato de propiedad. Flujo de control
Comprueba también dónde reside el fixture. Swift Testing crea una instancia de suite distinta para cada método de prueba de instancia y, de forma predeterminada, las pruebas de una suite se ejecutan en paralelo. El estado local a la instancia o a la prueba encaja con ese modelo; un fixture global o una colección estática introduce propiedad compartida entre pruebas. Organizar funciones de prueba con tipos de suite
En código de aplicaciones para Mac que requiera el actor principal, aísla explícitamente la prueba de Swift Testing con @MainActor. La guía de migración indica que Swift Testing no hereda el comportamiento predeterminado de XCTest de usar el actor principal en métodos de prueba síncronos. La calculadora sintética anterior no tiene ese requisito. Migrar una prueba de XCTest
El bloque diferido síncrono no sustituye al desmontaje esperado. Las acciones diferidas de Swift no pueden propagar un error fuera del bloque. Si la limpieza puede fallar, gestiona e informa explícitamente de ese fallo en vez de descartarlo. Mantén la limpieza esperada en una ruta de ejecución asíncrona y asegúrate de que se ejecute tanto si el cuerpo termina correctamente como si lanza un error. Manejo de errores
Para configurar y desmontar de forma reutilizable y gestionada por el framework, Swift Testing actual proporciona TestScoping, que ejecuta acciones personalizadas alrededor de una prueba o suite. Apple indica que está disponible desde Swift 6.1 y Xcode 16.3. Es una opción compatible cuando varias pruebas necesitan un ámbito común, pero requiere un trait personalizado y un proveedor de ámbito, en vez de limitarse a cambiar el nombre de addTeardownBlock. TestScoping
Diagnosticar una comprobación débil que cambia tras la migración
Si falla la prueba de propiedad migrada, inspecciona estos puntos en orden:
- Confirma el punto de observación. La liberación prevista debe ocurrir antes de la aserción débil.
- Inspecciona las propiedades del fixture. Otra propiedad puede seguir siendo propietaria de la calculadora después de que se borre una propiedad distinta.
- Inspecciona los alias locales y las capturas de cierres. Un objeto desenvuelto o una referencia capturada con fuerza puede prolongar la propiedad durante la comprobación.
- Inspecciona el ámbito del helper. La limpieza diferida de un helper puede ejecutarse demasiado pronto para el contrato previsto de la prueba.
- Inspecciona el trabajo pendiente. Si el objeto real inicia trabajo asíncrono, define y espera su finalización antes de aplicar una expectativa de liberación síncrona.
Una referencia débil distinta de nil identifica un objeto que aún existe en el punto de observación. Investiga quién sigue siendo su propietario antes de cambiar la aserción o añadir una demora. En este ejemplo sintético no hay trabajo asíncrono que esperar, y el control de diagnóstico muestra exactamente cómo afecta al resultado un propietario adicional.
La migración se completa cuando el cálculo y las aserciones de propiedad expresan el mismo resultado previsto que la prueba de referencia: calcular 12, eliminar la propiedad prevista del fixture y verificar la liberación mediante una referencia débil mantenida de forma independiente. Mantén visible esa secuencia a medida que evoluciona la prueba.
Fuentes
Configurar y desmontar el estado en las pruebas — Documentación para desarrolladores de Apple.
Migrar una prueba de XCTest — Proyecto Swift.
Desinicialización — Proyecto Swift, El lenguaje de programación Swift.
Conteo automático de referencias — Proyecto Swift, El lenguaje de programación Swift.
Añadir pruebas al proyecto de Xcode — Documentación para desarrolladores de Apple.
Flujo de control — Proyecto Swift, El lenguaje de programación Swift.
Expectativas y confirmaciones — Proyecto Swift.
Manejo de errores — Proyecto Swift, El lenguaje de programación Swift.
Organizar funciones de prueba con tipos de suite — Proyecto Swift.
TestScoping — Documentación para desarrolladores de Apple.