wandres.dev
SWIFT TESTING · el framework moderno

Precondición y verificación: el papel de require

La distinción entre comprobar una propiedad y exigir una condición para poder seguir. Semántica de `#require` como expectativa que lanza, desenvolvimiento seguro de opcionales en el sitio del uso, granularidad frente al antiguo interruptor global de XCTest y criterio para decidir qué verbo corresponde a cada línea.

⏱ 17 min

Una prueba que falla puede fallar de dos maneras muy distintas y confundirlas cuesta caro. En un caso, el sistema hace algo diferente de lo que afirmaste: hay información valiosa en ese fallo y conviene seguir ejecutando para recoger toda la que quepa. En el otro, la prueba no ha podido llegar siquiera a comprobar nada, porque el objeto que iba a examinar no existe, el fichero de datos no cargó o la búsqueda devolvió vacío: aquí insistir no aporta evidencia, solo ruido, y a menudo un cierre forzoso. Swift Testing separa ambos casos en dos verbos con semántica de control de flujo diferente, y esa separación —que en XCTest era un interruptor global y torpe— se convierte en una herramienta de diseño de pruebas mucho más fina de lo que parece.

🎯 Al terminar esta lección sabrás
  • Distinguir la semántica de continuación de #expect de la semántica de interrupción de #require.
  • Usar try #require para desenvolver opcionales y obtener el valor desenvuelto en la misma expresión.
  • Explicar por qué la prueba debe declararse throws y qué error propaga la macro al corredor.
  • Aplicar un criterio estable para decidir cuándo una línea es precondición y cuándo es verificación.

Dos verbos, dos consecuencias

#expect registra el fallo y sigue. #require registra el fallo y lanza, lo que aborta la función de prueba en ese punto exacto. Todo lo demás es idéntico: la misma expansión de macro, la misma captura de valores intermedios, el mismo diagnóstico.

@Test func elPedidoSeCierraCorrectamente() throws {
    let pedido = try #require(repositorio.buscar(id: 91))   // precondición
    #expect(pedido.estado == .cerrado)                      // verificación
    #expect(pedido.lineas.count == 3)                       // verificación
    #expect(pedido.total == 148)                            // verificación
}

Si el repositorio no encuentra el pedido, la primera línea corta la prueba con un mensaje claro y las tres siguientes no llegan a ejecutarse: no habría nada que verificar. Si en cambio el pedido existe pero está mal construido, las tres expectativas se evalúan todas y el informe muestra los tres fallos a la vez, que es la fotografía útil para diagnosticar.

Ese último punto merece énfasis porque contradice el instinto. Acumular fallos no es tolerancia al error: es maximizar la información obtenida por cada ejecución. Una prueba que se detiene en la primera diferencia obliga a un ciclo de corregir y volver a ejecutar por cada síntoma; una que las recoge todas te entrega el patrón completo, y el patrón suele señalar la causa mucho mejor que cualquiera de sus manifestaciones aisladas.

La simetría entre ambas macros llega también a las variantes para errores, donde la elección del verbo conserva exactamente el mismo significado:

// verificación: comprobar que lanza lo correcto y seguir mirando otras cosas
#expect(throws: ErrorPago.tarjetaCaducada) { try pasarela.cobrar(tarjeta) }

// precondición: si esto no lanza, el resto de la prueba examinaría un estado imposible
try #require(throws: ErrorRed.self) { try cliente.sincronizar() }

La forma con requerimiento devuelve además el error lanzado, lo que permite encadenar sobre él las comprobaciones de detalle sin repetir la llamada ni recurrir a un bloque de captura escrito a mano.

⚠️
El interruptor global que ya no hace falta

XCTest ofrecía continueAfterFailure, una propiedad booleana de la clase de pruebas que se aplicaba a todas las aserciones por igual. O todas cortaban o ninguna lo hacía, y la decisión se tomaba lejos del sitio donde importaba. #expect y #require mueven esa elección a cada línea, que es donde vive el conocimiento sobre si lo que sigue tiene sentido sin lo anterior.

Desenvolver en el sitio del uso

La variante más frecuente de #require no comprueba un booleano: recibe un opcional y devuelve el valor desenvuelto. Es el sustituto directo de XCTUnwrap, y su valor está en que elimina de las pruebas el único lugar donde el desenvolvimiento forzoso parecía aceptable.

@Test func laRespuestaTraeUnaCabeceraDeCache() throws {
    let respuesta = try #require(cliente.ultimaRespuesta)
    let cabecera  = try #require(respuesta.cabeceras["Cache-Control"])
    #expect(cabecera.contains("max-age"))
}

Comparado con respuesta!, el cambio no es cosmético. El desenvolvimiento forzoso provoca un cierre forzoso del proceso, y en un corredor que ejecuta muchas pruebas en paralelo dentro del mismo proceso eso significa perder también los resultados de todo lo demás. #require convierte esa catástrofe en un fallo localizado, atribuido a una prueba concreta, con nombre de fichero y línea.

Como el resultado de una conversión condicional también es un opcional, la misma macro cubre el estrechamiento de tipo sin ninguna sintaxis nueva:

let respuestaHTTP = try #require(respuesta as? RespuestaHTTP)
#expect(respuestaHTTP.codigo == 200)

Y funciona con expresiones booleanas cuando la condición es una precondición pura, aunque en ese uso no devuelve nada aprovechable:

try #require(servidorDePruebas.estaEncendido)

Como la macro lanza, la función de prueba debe declararse throws. El error que propaga no es un error de tu dominio sino un valor interno del framework que el corredor intercepta y traduce a un fallo; por eso no debes capturarlo con do y catch para inspeccionarlo, y por eso tampoco tiene sentido envolver una expectativa en un try?, que silenciaría exactamente la señal que querías.

flowchart TD
A[Comienza la prueba] --> B[require: precondicion]
B -->|se cumple| C[expect: verificacion 1]
B -->|falla| Z[Fallo registrado y salida inmediata]
C --> D[expect: verificacion 2]
D --> E[expect: verificacion 3]
C -.->|falla y continua| D
D -.->|falla y continua| E
E --> F[Informe con todos los fallos acumulados]
Z --> G[Informe con un unico fallo y causa clara]

El criterio: qué es precondición y qué es verificación

La regla operativa es breve y aguanta bien: usa #require cuando el fallo haría que las líneas siguientes carecieran de sentido, y #expect en todo lo demás. Dicho de otro modo, #require protege el andamiaje de la prueba; #expect juzga el comportamiento del sistema.

De ahí se derivan las decisiones difíciles. Obtener un elemento de un diccionario para examinarlo después es precondición. Comprobar que ese diccionario tiene exactamente cinco entradas es verificación, aunque te apetezca cortar. Cargar el fichero de datos de la prueba es precondición. Comprobar que el analizador lo interpreta bien es verificación, y si además vas a mirar tres campos del resultado, esos tres son expectativas independientes que quieres ver fallar juntas.

Hay un antipatrón muy extendido que este criterio disuelve: la cadena de expectativas defensivas, donde cada línea comprueba con #expect algo que la siguiente da por hecho, y un fallo temprano produce una cascada de fallos derivados que enmascaran el problema real. Ese ruido es exactamente lo que #require existe para eliminar, y reconocerlo en una prueba ajena es una de las señales más fiables de que fue traducida mecánicamente desde XCTest.

En el extremo opuesto está el abuso: una prueba escrita entera con #require recupera el comportamiento de detenerse al primer fallo y renuncia a la información que podría haber recogido. Si al revisar una prueba ves más requerimientos que expectativas, casi siempre significa que está haciendo demasiadas cosas y que conviene partirla.

Hay además una asimetría de coste que inclina la decisión en los casos dudosos. Convertir una expectativa en requerimiento es barato de revertir y su efecto se nota de inmediato; dejar como expectativa algo que era precondición produce fallos derivados que solo se identifican leyendo con atención el informe completo, y ese trabajo lo paga siempre otra persona en otro momento. Ante la duda, protege el andamiaje.

El tercer verbo: registrar sin afirmar

Ni todo fallo nace de una comparación ni toda comparación se puede escribir como expresión. Para esos huecos existe Issue.record, que registra un fallo con el mensaje que le des sin necesidad de que haya una expectativa detrás.

@Test func elAnalizadorRechazaEntradasCorruptas() throws {
    switch Analizador.procesar(entradaCorrupta) {
    case .error(let e):
        #expect(e.codigo == .formatoInvalido)
    case .exito:
        Issue.record("el analizador aceptó una entrada que debía rechazar")
    }
}

Su lugar natural son las ramas inalcanzables por construcción: el caso de una enumeración que no debería darse, la rama por defecto de una condición exhaustiva, el bloque catch que captura un error que la prueba no esperaba. Escribir ahí un #expect(false) funciona pero informa peor, porque el diagnóstico que la macro puede reconstruir es literalmente que un literal falso es falso.

Issue.record no interrumpe, con lo que su semántica de flujo es la de #expect. Para cortar en una rama imposible se combina con un return, o mejor se reformula la prueba de manera que la condición vuelva a ser un requerimiento explícito al principio, que casi siempre es la versión más legible.

Vale la pena distinguirlo de una tercera situación que se le parece y no lo es: cuando el fallo no es del sistema ni del andamiaje, sino que la prueba no debía ejecutarse en estas condiciones. Ese caso no se registra como incidencia, se declara como condición de ejecución con los traits que veremos al tratar las suites, y confundirlos llena los informes de fallos que ninguna corrección puede eliminar.

🧭

La pregunta correcta

¿Tiene sentido la línea siguiente si esta falla? Si la respuesta es no, es #require. Si es sí, es #expect.

🎁

Devuelve el valor

try #require sobre un opcional entrega el valor desenvuelto, con lo que el desenvolvimiento forzoso deja de tener excusa incluso en pruebas.

📉

Menos ruido, no menos rigor

Cortar pronto no oculta fallos: evita fallos derivados que no aportan información y que dificultan localizar la causa.

Dos modos de fallo y por qué la distinción es epistemológica

Detrás de la elección entre estos dos verbos hay una distinción que precede al framework y que la literatura sobre pruebas lleva décadas nombrando de formas distintas: la diferencia entre un test que refuta una afirmación sobre el sistema y un test que sencillamente no ha podido correr. Son estados epistémicos incomparables. En el primero has obtenido conocimiento positivo —el sistema hace algo distinto de lo que dijiste, y el informe describe qué— y ese conocimiento se puede acumular, porque saber que fallan tres propiedades relacionadas es estrictamente más informativo que saber que falla una. En el segundo no has obtenido nada sobre el sistema bajo prueba, solo sobre el entorno de la prueba, y todo lo que ejecutes a partir de ahí examina un mundo que no es el que querías examinar; peor aún, los fallos que produzcas después son artefactos de tu propio andamiaje y compiten por atención con los fallos reales. Confundir ambos modos tiene un coste medible en los equipos: una suite que produce cascadas de fallos derivados entrena a la gente a leer solo el primero, y a partir de ahí a desconfiar del resto, que es el primer paso hacia una suite que nadie mira. XCTest reconocía la distinción pero la resolvía con el instrumento equivocado, un interruptor por clase que obligaba a decidir globalmente algo que solo se sabe localmente, y por eso casi nadie lo tocaba. Que Swift Testing la codifique como dos macros con semánticas de control de flujo distintas convierte una decisión difusa de estilo en una decisión explícita y revisable línea por línea, y de paso hace que la forma de una prueba comunique su estructura lógica: la sección de requerimientos delimita el conjunto de mundos en los que la afirmación tiene sentido, y la sección de expectativas es la afirmación propiamente dicha. Leída así, una prueba bien escrita es un enunciado condicional cuyo antecedente son sus requerimientos, y la mayoría de las pruebas frágiles lo son porque su antecedente estaba implícito, escondido en un desenvolvimiento forzoso o en una suposición que nadie enunció.

⚔️ Reescribe el flujo de tus pruebas
  1. Localiza en tu suite una prueba con un desenvolvimiento forzoso y sustitúyelo por try #require, observando el cambio en el informe.
  2. Encuentra una cascada de fallos derivados y elimínala convirtiendo en requerimiento la primera línea de la cadena.
  3. Escribe una prueba con tres expectativas independientes que fallen a la vez y comprueba que el informe las muestra todas.
  4. Usa try #require sobre una conversión condicional para estrechar un tipo sin recurrir a la forzosa.
  5. Revisa una prueba escrita solo con requerimientos y decide si el remedio es cambiar verbos o partirla en dos.