Per riprodurre gli input casuali dei test Swift sul tuo Mac, passa un seed esplicito a un generatore nuovo per ogni caso di test. Mantieni invariati il generatore, la mappatura degli input e l’ordine delle estrazioni, quindi registra il seed insieme a ogni errore.
Questa guida crea un piccolo pacchetto Swift locale che verifica un calcolo aritmetico sintetico. Include un esempio completo con Swift Testing e un’alternativa con XCTest. Tutti i dati di test provengono da calcoli locali; il pacchetto non ha dipendenze esterne.
Cosa rende ripetibile il seed?
Esistono due fonti distinte di variazione: i valori generati da un test e l’ordine in cui il runner pianifica i test. Un seed definito dall’applicazione controlla solo la prima, e solo se ogni valore generato proviene dal generatore corrispondente. Apple documenta che le normali operazioni casuali usano per impostazione predefinita SystemRandomNumberGenerator con seed automatico. Il suo inizializzatore documentato non accetta seed. SystemRandomNumberGenerator
Xcode offre anche l’esecuzione in ordine casuale per individuare dipendenze tra test. La documentazione dei piani di test descrive l’esecuzione in ordine alfabetico o casuale, ma non documenta un seed di riproduzione fornito dall’utente per questa impostazione. Il metodo supportato usato qui consiste quindi nel generare gli input con un generatore con seed esplicito all’interno di test indipendenti. Non garantisce di riprodurre la pianificazione del runner. Migliorare la valutazione del codice organizzando i test in piani di test
Usa questa guida decisionale prima di modificare un test instabile:
| Cosa varia tra un’esecuzione e l’altra? | Cosa controllare |
|---|---|
| Input numerici generati | Seed, implementazione del generatore, mappatura degli input e ordine delle estrazioni |
| Risultati dopo l’esecuzione di un altro test | Stato mutabile e inizializzazione per ogni test |
| Risultati solo durante l’esecuzione concorrente | Risorse mutabili condivise e attività sovrapposte |
| Input dopo un aggiornamento della toolchain | Implementazione della mappatura degli input e input concreti registrati |
Swift supporta esplicitamente i generatori personalizzati tramite RandomNumberGenerator, la cui operazione richiesta è un next() mutante che restituisce un UInt64. La documentazione nel codice sorgente del progetto Swift indica le sequenze ripetibili per i test come motivo per fornire un generatore con seed. Random.swift
Crea un piccolo pacchetto di test locale
Usa un Mac con Xcode e una toolchain Swift 6 o successiva disponibile. Controlla in Terminale la toolchain selezionata per la riga di comando, quindi crea una nuova directory e inizializza un pacchetto di libreria:
swift --version
mkdir SeedReplay
cd SeedReplay
swift package init --type library
I test di integrazione del progetto Swift verificano la creazione di pacchetti di libreria con swift package init --type library. Swift 6 include Swift Testing direttamente, quindi questo esempio non richiede dipendenze del pacchetto per import Testing. Swift Package Manager esegue sia i test Swift Testing sia quelli XCTest, se presenti. Test di integrazione per l’inizializzazione di pacchetti Swift, Annuncio di Swift 6
Mantieni il manifest del pacchetto e il codice sorgente della libreria generati. Sostituisci il file di test di esempio generato con i file di test riportati sotto, tutti in Tests/SeedReplayTests. La funzione aritmetica si trova nel file di supporto dei test, così l’esempio resta autonomo. In un progetto esistente, usa invece la funzione effettiva da testare.
Definisci il seed e il contratto di generazione degli input
Crea Tests/SeedReplayTests/ReplaySupport.swift con questo contenuto 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 configurazione accetta un UInt64 decimale, incluso zero. Se TEST_SEED non è impostato, vengono selezionati tre seed espliciti; un valore vuoto, negativo, non numerico o fuori intervallo genera un errore. Un seed digitato in modo errato non può quindi selezionare silenziosamente un’esecuzione diversa.
Il generatore usa operazioni aritmetiche esplicite con wraparound. Ogni input generato prende i dieci bit più significativi di un output, producendo valori da 0 a 1023. Questa mappatura è definita intenzionalmente nel codice sorgente. L’aritmetica resta contenuta: il risultato atteso più grande è 523776. Si tratta di una ricetta di input sintetici con limiti definiti, senza pretese di copertura statistica dei test.
Perché qui evitare Int.random(in:using:)? Questa API supporta un generatore fornito dall’utente ed è utile per riprodurre i risultati con una toolchain fissa. Tuttavia, Apple avverte esplicitamente che l’algoritmo di generazione dei valori può cambiare tra versioni di Swift, anche quando un generatore produce la stessa sequenza sottostante. Definire autonomamente la piccola mappatura di questo esempio elimina questa specifica dipendenza. Int.random(in:using:)
Esegui e riproduci la versione 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])
}
Il test parametrizzato tratta ogni seed come un caso di test separato. Apple documenta che arguments: fornisce gli elementi di una raccolta alle invocazioni dei test e consente espressioni che generano errori. In questo modo la selezione dei seed è visibile nel modello dei test. L’indice esplicito del ciclo identifica poi un singolo input generato all’interno di quel seed. Implementare test parametrizzati
Esegui la raccolta predefinita, quindi riproduci due volte un seed:
swift test
TEST_SEED=42 swift test
TEST_SEED=42 swift test
Queste due esecuzioni con seed dovrebbero generare gli stessi input numerici se il codice sorgente non cambia. Per il seed 42, i primi tre input sono 759, 163 e 285. Il test verifica 256 input per quel seed. Il test di stabilità separato usa intenzionalmente sempre il seed zero: verifica il generatore e la mappatura indipendentemente dal seed scelto per il test aritmetico.
Il risultato atteso dell’implementazione fornita è che tutte le asserzioni passino. L’ambiente di ricerca non disponeva di Swift, quindi i file Swift sono stati esaminati senza compilarli. Un calcolo Python indipendente ha verificato tutti i 768 confronti aritmetici per i tre seed predefiniti e i valori iniziali mostrati; tale verifica non sostituisce la compilazione del pacchetto Swift.
Esercitati a riprodurre un errore di calcolo intenzionale
Per vedere il percorso diagnostico, sostituisci temporaneamente l’espressione di ritorno in triangularNumber(_:) con questo frammento etichettato:
// Temporary replacement inside triangularNumber(_:).
return n * n / 2
Esegui di nuovo TEST_SEED=42 swift test. Il primo input generato è 759. L’espressione modificata restituisce 288040; il totale di riferimento è 288420. Il messaggio dell’asserzione indica il seed 42, l’indice del caso 0 e l’input 759. Il ritorno anticipato interrompe quell’invocazione dopo la prima discrepanza registrata, mantenendo la diagnosi mirata.
Ripristina return n * (n + 1) / 2 ed esegui di nuovo lo stesso comando. Mantieni costante il seed mentre valuti la correzione. Quando un input generato porta alla luce un difetto utile, conserva anche quell’input concreto in un test di regressione con nome: resterà comprensibile anche se la ricetta di generazione dovesse cambiare in seguito.
Applica lo stesso ciclo di vita a XCTest
Per una destinazione basata su XCTest, mantieni ReplaySupport.swift e usa il seguente file di test completo al posto del file Swift Testing. Chiamalo 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]
)
}
}
Si applicano gli stessi comandi di Terminale. Ogni chiamata a generatedInputs(seed:) crea un generatore nuovo, quindi nessun test consuma il flusso casuale di un altro test. Non c’è stato del generatore da ripulire dopo il test.
Se una fixture XCTest esistente conserva un generatore come proprietà, reimpostalo prima di ogni metodo di test. Apple documenta i metodi di inizializzazione dell’istanza per l’inizializzazione per ogni test e l’inizializzazione della classe per lo stato condiviso da tutta la classe. Quando i test devono poter essere riprodotti in modo indipendente, un flusso casuale mutabile va inizializzato per ogni test. Preparare lo stato del test successivo durante lo smontaggio è inaffidabile, perché lo smontaggio non è garantito dopo un arresto anomalo. Impostare e ripristinare lo stato nei test
Tieni separata la pianificazione di Xcode dalla riproduzione degli input
Per eseguire questo pacchetto da Xcode, apri il suo Package.swift. Per un progetto che usa un piano di test, scegli Product > Scheme > Edit Test Plan, seleziona la configurazione pertinente e aggiungi TEST_SEED con il valore decimale 42 in Environment Variables. Esegui con Product > Test e controlla il seed stampato nell’output dei test. Apple documenta la configurazione delle variabili d’ambiente nei piani di test e la selezione del piano attivo prima di eseguire i test. Migliorare la valutazione del codice organizzando i test in piani di test
Verifica il seed stampato ogni volta che passi da Terminale a Xcode o viceversa. Una variabile assegnata a un singolo comando di Terminale non è un’impostazione del piano di test di Xcode. La riga nel log indica quale configurazione ha effettivamente letto il processo di test.
Swift Testing esegue i test in parallelo per impostazione predefinita. Il tratto .serialized può serializzare i casi all’interno di un test parametrizzato oppure i test all’interno di una suite, ma non controlla i test non correlati. Apple documenta anche swift test --no-parallel per disabilitare la parallelizzazione a livello globale. Nessuno dei due controlli fornisce un seed per gli input. Questo esempio non richiede la serializzazione perché ogni caso gestisce il proprio generatore e i propri input numerici. Eseguire i test in serie o in parallelo
Se un test passa da solo ma fallisce insieme agli altri nonostante gli stessi input generati, esamina lo stato condiviso. L’esecuzione seriale può aiutare a indagare le attività sovrapposte, ma un seed non può ripristinare gli oggetti mutabili di un altro test né riprodurre un’intera pianificazione concorrente.
Cosa conservare insieme a un seed che riproduce un errore?
Conserva informazioni sufficienti per ricostruire la sequenza di input:
- Il nome del test e il seed decimale.
- La versione del generatore e della mappatura, qui
upper10-v1. - L’indice del caso e l’input concreto riportati nel messaggio dell’asserzione.
- La revisione del codice sorgente, la versione di Swift e la versione di Xcode selezionata.
- Il numero di input e ogni modifica al modo in cui vengono consumate le estrazioni casuali.
In questo esempio, aggiungere un’estrazione dal generatore prima del ciclo sposta tutti gli input successivi. Cambiare l’entità dello spostamento modifica la mappatura. Aumentare il numero di input mantiene invariato il prefisso esistente solo se il generatore e la mappatura restano invariati. Queste sono conseguenze del contratto di riproduzione definito dal codice.
Mantieni una piccola raccolta fissa di seed per le esecuzioni abituali, prova altri seed espliciti durante l’esplorazione e salva gli input concreti di regressione quando rivelano un difetto. Una riproduzione utile parte dagli stessi valori e da uno stato iniziale noto; il seed diventa affidabile perché il test controlla come viene usato.
Fonti
- SystemRandomNumberGenerator — Apple.
- Migliorare la valutazione del codice organizzando i test in piani di test — Apple.
- Random.swift — Progetto Swift.
- swift-package-init-lib.md — Progetto Swift.
- Annuncio di Swift 6 — Swift.org.
- random(in:using:) — Apple.
- Implementare test parametrizzati — Apple.
- Impostare e ripristinare lo stato nei test — Apple.
- Eseguire i test in serie o in parallelo — Apple.