Para volver a ejecutar entradas aleatorias de pruebas de Swift en tu Mac, pasa una semilla explícita a un generador nuevo para cada caso de prueba. Mantén fijos el generador, la asignación de entradas y el orden de las extracciones; luego registra la semilla junto a cada fallo.
Esta guía crea un pequeño paquete local de Swift que comprueba un cálculo aritmético sintético. Incluye un ejemplo completo con Swift Testing y una alternativa con XCTest. Todos los datos de prueba proceden de cálculos locales; el paquete no tiene dependencias externas.
¿Qué hace repetible realmente la semilla?
Hay dos fuentes de variación distintas: los valores que genera una prueba y el orden en que el ejecutor programa las pruebas. Una semilla definida por la aplicación controla solo la primera, y únicamente cuando todos los valores generados provienen del generador correspondiente. Apple documenta que las operaciones aleatorias habituales usan SystemRandomNumberGenerator, con inicialización automática, de forma predeterminada. Su inicializador documentado no acepta una semilla. SystemRandomNumberGenerator
Xcode también ofrece un orden de ejecución aleatorio para revelar dependencias entre pruebas. Su documentación sobre los planes de prueba describe un orden alfabético o aleatorio, pero no documenta una semilla de reproducción proporcionada por el usuario para esa opción. Por ello, el método admitido que se usa aquí es un generador de entradas con semilla explícita dentro de pruebas independientes. No garantiza reproducir la programación del ejecutor. Mejorar la evaluación del código organizando las pruebas en planes de prueba
Usa esta guía de decisiones antes de modificar una prueba inestable:
| ¿Qué varía entre ejecuciones? | Qué controlar |
|---|---|
| Entradas numéricas generadas | Semilla, implementación del generador, asignación de entradas y orden de las extracciones |
| Resultados después de ejecutar otra prueba | Estado mutable e inicialización por prueba |
| Resultados solo durante la ejecución simultánea | Recursos mutables compartidos y trabajo superpuesto |
| Entradas después de actualizar la cadena de herramientas | Implementación de la asignación de entradas y entradas concretas registradas |
Swift admite explícitamente generadores personalizados mediante RandomNumberGenerator, cuya operación obligatoria es un next() mutante que devuelve UInt64. La documentación del código fuente del proyecto Swift señala que ofrecer secuencias repetibles para las pruebas es uno de los motivos para proporcionar un generador con semilla. Random.swift
Crea un pequeño paquete de prueba local
Usa un Mac con Xcode y una cadena de herramientas de Swift 6 o posterior disponible. Comprueba en Terminal la cadena de herramientas seleccionada; después, crea un directorio nuevo e inicializa un paquete de biblioteca:
swift --version
mkdir SeedReplay
cd SeedReplay
swift package init --type library
Las pruebas de integración del proyecto Swift comprueban la creación de paquetes de biblioteca con swift package init --type library. Swift 6 incluye Swift Testing directamente, así que este ejemplo no necesita ninguna dependencia del paquete para import Testing. Swift Package Manager ejecuta tanto las pruebas de Swift Testing como las de XCTest, si las hay. Prueba de integración de inicialización de paquetes Swift, Presentamos Swift 6
Conserva el manifiesto y el código fuente de biblioteca generados para el paquete. Sustituye el archivo de prueba de ejemplo generado por los archivos de prueba siguientes, todos dentro de Tests/SeedReplayTests. La función aritmética está en el archivo de soporte de pruebas para que esta demostración sea autocontenida. En un proyecto existente, usa en su lugar la función real que estés probando.
Define el contrato de semilla y generación de entradas
Crea Tests/SeedReplayTests/ReplaySupport.swift con este contenido completo:
import Foundation
// Local synthetic test-data generator.
// Mutable value state: give each test case its own instance.
// Its output sequence is part of this example's replay contract.
struct ReplayGenerator: RandomNumberGenerator {
private var state: UInt64
init(seed: UInt64) {
state = seed
}
mutating func next() -> UInt64 {
state &+= 0x9E3779B97F4A7C15
var value = state
value = (value ^ (value >> 30)) &* 0xBF58476D1CE4E5B9
value = (value ^ (value >> 27)) &* 0x94D049BB133111EB
return value ^ (value >> 31)
}
}
enum ReplayConfigurationError: Error {
case invalidSeed(String)
}
func replaySeeds() throws -> [UInt64] {
guard let text = ProcessInfo.processInfo.environment["TEST_SEED"] else {
return [0, 42, 2026]
}
guard !text.isEmpty,
text.utf8.allSatisfy({ $0 >= 48 && $0 <= 57 }),
let seed = UInt64(text) else {
throw ReplayConfigurationError.invalidSeed(text)
}
return [seed]
}
func generatedInputs(seed: UInt64) -> [Int] {
var generator = ReplayGenerator(seed: seed)
return (0..<256).map { _ in
// Exactly one draw per input; retain the upper ten bits.
Int(generator.next() >> 54)
}
}
func triangularNumber(_ n: Int) -> Int {
precondition((0...1023).contains(n))
return n * (n + 1) / 2
}
func referenceTotal(_ n: Int) -> Int {
precondition((0...1023).contains(n))
return (0...n).reduce(0, +)
}
La configuración acepta un UInt64 decimal, incluido el cero. Si no se define TEST_SEED, se seleccionan tres semillas explícitas; un valor vacío, negativo, no numérico o fuera de rango genera un error. Así, una semilla mal escrita no puede seleccionar silenciosamente otra ejecución.
El generador usa aritmética explícita con desbordamiento permitido. Cada entrada generada toma los diez bits superiores de una salida, lo que produce valores desde 0 hasta 1023. Esa asignación se mantiene deliberadamente fija en el código fuente. El cálculo es pequeño: el resultado esperado máximo es 523776. Se trata de una receta acotada de entradas sintéticas, sin pretender ofrecer cobertura estadística de las pruebas.
¿Por qué evitar Int.random(in:using:) aquí? Esa API admite un generador proporcionado y resulta útil para repetir ejecuciones con una cadena de herramientas fija. Sin embargo, Apple advierte explícitamente que el algoritmo de generación de valores puede cambiar entre versiones de Swift, incluso con un generador que produzca la misma secuencia subyacente. Al definir la pequeña asignación de este ejemplo directamente, se elimina esa dependencia concreta. Int.random(in:using:)
Ejecuta y reproduce la versión con Swift Testing
Crea Tests/SeedReplayTests/SeedReplayTests.swift:
import Testing
@Test(arguments: try replaySeeds())
func triangularTotals(seed: UInt64) {
print("triangularTotals seed=\(seed) recipe=upper10-v1 count=256")
for (index, n) in generatedInputs(seed: seed).enumerated() {
let actual = triangularNumber(n)
let expected = referenceTotal(n)
#expect(
actual == expected,
"seed=\(seed) case=\(index) n=\(n) actual=\(actual) expected=\(expected)"
)
if actual != expected {
return
}
}
}
@Test
func replayRecipeIsStable() {
var generator = ReplayGenerator(seed: 0)
let firstWord = generator.next()
#expect(firstWord == 0xE220A8397B1DCDAF)
let firstInputs = Array(generatedInputs(seed: 0).prefix(3))
#expect(firstInputs == [904, 441, 27])
}
La prueba parametrizada convierte cada semilla en un caso de prueba independiente. Apple documenta que arguments: proporciona elementos de una colección a las invocaciones de prueba y permite expresiones que generan errores. Esto hace que la selección de semillas sea visible en el modelo de pruebas. El índice explícito del bucle identifica después cada entrada generada dentro de esa semilla. Implementación de pruebas parametrizadas
Ejecuta la colección predeterminada y luego reproduce dos veces una semilla:
swift test
TEST_SEED=42 swift test
TEST_SEED=42 swift test
Estas dos ejecuciones con semilla deberían generar las mismas entradas numéricas si el código fuente no cambia. Para la semilla 42, las tres primeras entradas son 759, 163 y 285. La prueba comprueba 256 entradas para esa semilla. La prueba de estabilidad independiente mantiene intencionadamente la semilla cero: comprueba el generador y la asignación, independientemente de la semilla seleccionada para la prueba aritmética.
El resultado esperado de la implementación proporcionada es que todas las aserciones pasen. Swift no estaba disponible en el entorno de investigación, así que se inspeccionaron los archivos de Swift en lugar de compilarlos. Un cálculo independiente en Python comprobó las 768 comparaciones aritméticas de las tres semillas predeterminadas y los valores iniciales mostrados; esa comprobación no sustituye la compilación del paquete de Swift.
Practica la reproducción de un error de cálculo deliberado
Para ver la ruta de diagnóstico, sustituye temporalmente la expresión de retorno de triangularNumber(_:) por este fragmento etiquetado:
// Temporary replacement inside triangularNumber(_:).
return n * n / 2
Vuelve a ejecutar TEST_SEED=42 swift test. La primera entrada generada es 759. La expresión modificada da 288040; el total de referencia es 288420. El mensaje de aserción identifica la semilla 42, el índice del caso 0 y la entrada 759. El retorno anticipado detiene esa invocación después de registrar la primera discrepancia, de modo que el diagnóstico se mantenga centrado.
Restaura return n * (n + 1) / 2 y vuelve a ejecutar el mismo comando. Mantén constante la semilla mientras evalúas la corrección. Cuando una entrada generada haya revelado un defecto útil, conserva también esa entrada concreta en una prueba de regresión con nombre; seguirá siendo comprensible aunque la receta de generación cambie más adelante.
Aplica el mismo ciclo de vida a XCTest
Para un destino basado en XCTest, conserva ReplaySupport.swift y usa el siguiente archivo de prueba completo en lugar del archivo de Swift Testing. Llámalo Tests/SeedReplayTests/SeedReplayXCTests.swift:
import XCTest
final class SeedReplayXCTests: XCTestCase {
func testTriangularTotals() throws {
for seed in try replaySeeds() {
print("testTriangularTotals seed=\(seed) recipe=upper10-v1 count=256")
for (index, n) in generatedInputs(seed: seed).enumerated() {
let actual = triangularNumber(n)
let expected = referenceTotal(n)
XCTAssertEqual(
actual,
expected,
"seed=\(seed) case=\(index) n=\(n)"
)
if actual != expected {
return
}
}
}
}
func testReplayRecipeIsStable() {
var generator = ReplayGenerator(seed: 0)
XCTAssertEqual(generator.next(), 0xE220A8397B1DCDAF)
XCTAssertEqual(
Array(generatedInputs(seed: 0).prefix(3)),
[904, 441, 27]
)
}
}
Se aplican los mismos comandos de Terminal. Cada llamada a generatedInputs(seed:) crea un generador nuevo, así que ninguna prueba consume el flujo aleatorio de otra. No hay estado del generador que limpiar después de la prueba.
Si un fixture XCTest existente almacena un generador como propiedad, restablécelo antes de cada método de prueba. Apple documenta métodos de preparación de instancias para inicializar cada prueba y métodos de preparación de clase para el estado compartido por toda la clase. Si las pruebas deben reproducirse de forma independiente, el flujo aleatorio mutable debe inicializarse para cada prueba. Preparar en el desmontaje el estado de la prueba siguiente no es fiable, porque no se garantiza que el desmontaje se ejecute tras un fallo grave. Configurar y limpiar el estado en las pruebas
Mantén la programación de Xcode separada de la reproducción de entradas
Para ejecutar este paquete desde Xcode, abre su Package.swift. En un proyecto que use un plan de prueba, elige Product > Scheme > Edit Test Plan, selecciona la configuración correspondiente y añade TEST_SEED con el valor decimal 42 en Environment Variables. Ejecuta con Product > Test y confirma la semilla impresa en la salida de las pruebas. Apple documenta la configuración del entorno en los planes de prueba y la selección del plan activo antes de ejecutar las pruebas. Mejorar la evaluación del código organizando las pruebas en planes de prueba
Comprueba la semilla impresa cada vez que alternes entre Terminal y Xcode. Una variable asignada a un solo comando de Terminal no configura el plan de prueba de Xcode. La línea del registro indica qué configuración leyó realmente el proceso de prueba.
Swift Testing ejecuta las pruebas en paralelo de forma predeterminada. El rasgo .serialized puede serializar casos dentro de una prueba parametrizada o pruebas dentro de una suite, pero no controla pruebas ajenas. Apple también documenta swift test --no-parallel para desactivar globalmente el paralelismo. Ninguno de estos controles proporciona una semilla de entrada. Este ejemplo no necesita serialización porque cada caso tiene su propio generador y sus propias entradas numéricas. Ejecución de pruebas en serie o en paralelo
Si una prueba pasa por sí sola, pero falla junto con otras pese a que las entradas generadas son idénticas, examina el estado compartido. La ejecución en serie puede ayudar a investigar el trabajo superpuesto, pero una semilla no puede restablecer los objetos mutables de otra prueba ni reproducir un programa completo de ejecución concurrente.
¿Qué debes conservar junto a una semilla que produjo un fallo?
Conserva suficiente información para reconstruir la secuencia de entradas:
- El nombre de la prueba y la semilla decimal.
- La versión del generador y de la asignación, aquí
upper10-v1. - El índice del caso y la entrada concreta del mensaje de aserción.
- La revisión del código fuente, la versión de Swift y la versión de Xcode seleccionada.
- El número de entradas y cualquier cambio en la forma de consumir las extracciones aleatorias.
En este ejemplo, añadir una extracción del generador antes del bucle desplaza todas las entradas siguientes. Cambiar la cantidad del desplazamiento modifica la asignación. Aumentar el número de entradas conserva el prefijo existente solo mientras no cambien el generador ni la asignación. Estas son consecuencias del contrato de reproducción del código.
Mantén una pequeña colección fija de semillas para las ejecuciones habituales, prueba semillas explícitas adicionales al explorar y guarda entradas concretas de regresión cuando revelen un defecto. Una reproducción útil comienza con los mismos valores y un estado inicial conocido; la semilla resulta fiable porque la prueba controla cómo se consume.
Fuentes
- SystemRandomNumberGenerator — Apple.
- Mejorar la evaluación del código organizando las pruebas en planes de prueba — Apple.
- Random.swift — Proyecto Swift.
- swift-package-init-lib.md — Proyecto Swift.
- Presentamos Swift 6 — Swift.org.
- random(in:using:) — Apple.
- Implementación de pruebas parametrizadas — Apple.
- Configurar y limpiar el estado en las pruebas — Apple.
- Ejecución de pruebas en serie o en paralelo — Apple.