Pour rejouer les entrées aléatoires des tests Swift sur votre Mac, transmettez une graine explicite à un nouveau générateur pour chaque cas de test. Conservez le générateur, le mappage des entrées et l’ordre des tirages, puis consignez la graine avec chaque échec.
Ce guide crée un petit paquet Swift local qui vérifie un calcul arithmétique synthétique. Il fournit un exemple complet avec Swift Testing ainsi qu’une solution de remplacement avec XCTest. Toutes les données de test proviennent de calculs locaux ; le paquet n’a aucune dépendance externe.
Qu’est-ce que la graine rend réellement répétable ?
Il existe deux sources de variation distinctes : les valeurs générées par un test et l’ordre dans lequel l’exécuteur planifie les tests. Une graine définie par l’application contrôle uniquement la première, à condition que chaque valeur générée provienne du générateur correspondant. Apple indique que les opérations aléatoires ordinaires utilisent par défaut SystemRandomNumberGenerator, dont la graine est générée automatiquement. Son initialiseur documenté n’accepte aucune graine. SystemRandomNumberGenerator
Xcode propose également un ordre d’exécution aléatoire pour révéler les dépendances entre les tests. La documentation de son plan de test décrit un ordre d’exécution alphabétique ou aléatoire, mais ne documente pas de graine de rejeu fournie par l’utilisateur pour ce réglage. La méthode prise en charge ici consiste donc à utiliser un générateur d’entrées explicitement initialisé avec une graine dans des tests indépendants. Elle ne garantit pas de reproduire la planification de l’exécuteur. Améliorer l’évaluation du code en organisant les tests en plans de test
Utilisez cet aide-mémoire avant de modifier un test instable :
| Ce qui varie entre les exécutions | Élément à contrôler |
|---|---|
| Entrées numériques générées | Graine, implémentation du générateur, mappage des entrées et ordre des tirages |
| Résultats après l’exécution d’un autre test | État mutable et initialisation par test |
| Résultats uniquement lors d’une exécution concurrente | Ressources mutables partagées et travaux qui se chevauchent |
| Entrées après une mise à jour de la chaîne d’outils | Implémentation du mappage des entrées et entrées concrètes consignées |
Swift prend explicitement en charge les générateurs personnalisés via RandomNumberGenerator, dont l’opération requise est une méthode mutante next() qui renvoie un UInt64. La documentation source du projet Swift indique que la possibilité de produire des séquences répétables pour les tests est une raison de fournir un générateur initialisable avec une graine. Random.swift
Créer un petit paquet de test local
Utilisez un Mac avec Xcode et une chaîne d’outils Swift 6 ou ultérieure disponible. Vérifiez dans Terminal la chaîne d’outils sélectionnée, puis créez un nouveau répertoire et initialisez un paquet de bibliothèque :
swift --version
mkdir SeedReplay
cd SeedReplay
swift package init --type library
Les tests d’intégration du projet Swift vérifient la création d’un paquet de bibliothèque avec swift package init --type library. Swift 6 inclut directement Swift Testing ; cet exemple n’a donc besoin d’aucune dépendance de paquet pour import Testing. Swift Package Manager exécute à la fois les tests Swift Testing et XCTest, s’ils sont présents. Test d’intégration de l’initialisation d’un paquet Swift, Annonce de Swift 6
Conservez le manifeste du paquet généré et le code source de la bibliothèque. Remplacez le fichier de test d’exemple généré par les fichiers de test ci-dessous, tous dans Tests/SeedReplayTests. La fonction arithmétique se trouve dans le fichier de prise en charge des tests afin que cette démonstration reste autonome. Dans un projet existant, utilisez plutôt la fonction réellement testée.
Définir le contrat de graine et de génération des entrées
Créez Tests/SeedReplayTests/ReplaySupport.swift avec ce contenu complet :
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 configuration accepte un UInt64 décimal, y compris zéro. Si TEST_SEED n’est pas défini, trois graines explicites sont sélectionnées ; une valeur vide, négative, non numérique ou hors limites déclenche une erreur. Ainsi, une graine mal saisie ne peut pas sélectionner silencieusement une autre exécution.
Le générateur utilise une arithmétique explicite avec retour circulaire. Chaque entrée générée prend les dix bits de poids fort d’une sortie, ce qui donne des valeurs comprises entre 0 et 1023. Ce mappage est volontairement fixé dans le code source. Le calcul reste modeste : le résultat attendu maximal est 523776. Il s’agit d’une recette d’entrées synthétiques bornées, sans prétention de couverture statistique des tests.
Pourquoi éviter Int.random(in:using:) ici ? Cette API accepte un générateur fourni et convient pour le rejeu avec une chaîne d’outils fixe. Toutefois, Apple avertit explicitement que son algorithme de génération des valeurs peut changer d’une version de Swift à l’autre, même avec un générateur qui produit la même séquence sous-jacente. En définissant nous-mêmes le petit mappage de cet exemple, nous supprimons cette dépendance particulière. Int.random(in:using:)
Exécuter et rejouer la version Swift Testing
Créez 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])
}
Le test paramétré fait de chaque graine un cas de test distinct. Apple indique que arguments: fournit les éléments d’une collection aux invocations de test et autorise les expressions susceptibles de lever une erreur. Le choix des graines reste ainsi visible dans le modèle de test. L’indice explicite de la boucle identifie ensuite chaque entrée générée pour une graine donnée. Implémenter des tests paramétrés
Exécutez la collection par défaut, puis rejouez deux fois une graine :
swift test
TEST_SEED=42 swift test
TEST_SEED=42 swift test
Ces deux exécutions avec la même graine devraient générer les mêmes valeurs numériques si le code source reste inchangé. Pour la graine 42, les trois premières entrées sont 759, 163 et 285. Le test vérifie 256 entrées pour cette graine. Le test de stabilité distinct conserve intentionnellement la graine zéro : il vérifie le générateur et le mappage indépendamment de la graine choisie pour le test arithmétique.
Avec l’implémentation fournie, toutes les assertions devraient réussir. Swift n’était pas disponible dans l’environnement de recherche ; les fichiers Swift ont donc été examinés, mais pas compilés. Un calcul Python indépendant a vérifié les 768 comparaisons arithmétiques des trois graines par défaut ainsi que les premières valeurs affichées ; cette vérification ne remplace pas la compilation du paquet Swift.
S’exercer à rejouer une erreur de calcul délibérée
Pour observer le parcours de diagnostic, remplacez temporairement l’expression de retour dans triangularNumber(_:) par ce fragment annoté :
// Temporary replacement inside triangularNumber(_:).
return n * n / 2
Exécutez de nouveau TEST_SEED=42 swift test. La première entrée générée est 759. L’expression modifiée renvoie 288040 ; le total de référence est 288420. Le message d’assertion indique la graine 42, l’indice du cas 0 et l’entrée 759. Le retour anticipé arrête cette invocation dès la première divergence consignée, afin de cibler le diagnostic.
Rétablissez return n * (n + 1) / 2 et exécutez de nouveau la même commande. Gardez la même graine pendant l’évaluation de la correction. Lorsqu’une entrée générée a permis de révéler un défaut utile, conservez aussi cette entrée concrète dans un test de régression nommé ; elle restera compréhensible même si la recette de génération change par la suite.
Appliquer le même cycle à XCTest
Pour une cible XCTest, conservez ReplaySupport.swift et utilisez le fichier de test complet suivant à la place du fichier Swift Testing. Nommez-le 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]
)
}
}
Les mêmes commandes Terminal s’appliquent. Chaque appel à generatedInputs(seed:) construit un nouveau générateur, de sorte qu’aucun test ne consomme le flux aléatoire d’un autre. Il n’y a aucun état de générateur à nettoyer après le test.
Si une fixture XCTest existante stocke un générateur comme propriété, réinitialisez-le avant chaque méthode de test. Apple documente les méthodes d’initialisation d’instance pour l’initialisation par test et l’initialisation de classe pour l’état partagé dans toute la classe. Un flux aléatoire mutable doit être initialisé pour chaque test lorsque les tests doivent être rejoués indépendamment. Préparer l’état du test suivant dans la procédure de nettoyage n’est pas fiable, car celle-ci n’est pas garantie après un plantage. Configurer et nettoyer l’état dans vos tests
Distinguer la planification de Xcode du rejeu des entrées
Pour exécuter ce paquet depuis Xcode, ouvrez son fichier Package.swift. Pour un projet qui utilise un plan de test, choisissez Product > Scheme > Edit Test Plan, sélectionnez la configuration concernée, puis ajoutez TEST_SEED avec la valeur décimale 42 sous Environment Variables. Exécutez avec Product > Test et vérifiez la graine affichée dans les résultats du test. Apple documente la configuration de l’environnement d’un plan de test et la sélection du plan actif avant l’exécution des tests. Améliorer l’évaluation du code en organisant les tests en plans de test
Vérifiez la graine affichée chaque fois que vous passez de Terminal à Xcode. Une variable attribuée à une seule commande Terminal ne constitue pas un réglage du plan de test Xcode. La ligne de journal indique la configuration effectivement lue par le processus de test.
Swift Testing exécute les tests en parallèle par défaut. Son trait .serialized peut sérialiser les cas d’un test paramétré ou les tests d’une suite, mais il ne régit pas les tests sans lien entre eux. Apple documente également swift test --no-parallel pour désactiver globalement la parallélisation. Aucun de ces réglages ne fournit une graine d’entrée. Cet exemple n’a pas besoin de sérialisation, car chaque cas possède son propre générateur et ses propres entrées numériques. Exécuter les tests en série ou en parallèle
Si un test réussit seul, mais échoue avec d’autres tests malgré des entrées générées identiques, examinez l’état partagé. L’exécution en série peut aider à étudier les travaux qui se chevauchent, mais une graine ne peut ni réinitialiser les objets mutables d’un autre test ni reproduire un ordonnancement concurrent complet.
Que conserver avec une graine associée à un échec ?
Conservez suffisamment d’informations pour reconstituer la séquence d’entrées :
- Le nom du test et la graine décimale.
- La version du générateur et du mappage, ici
upper10-v1. - L’indice du cas et l’entrée concrète indiqués dans le message d’assertion.
- La révision du code source, la version de Swift et la version de Xcode sélectionnée.
- Le nombre d’entrées et tout changement dans la consommation des tirages aléatoires.
Dans cet exemple, ajouter un tirage du générateur avant la boucle décale toutes les entrées suivantes. Modifier la quantité de décalage change le mappage. Augmenter le nombre d’entrées conserve le préfixe existant uniquement tant que le générateur et le mappage restent inchangés. Ces conséquences découlent du contrat de rejeu du code.
Conservez une petite collection fixe de graines pour les exécutions habituelles, essayez d’autres graines explicites lors de vos explorations et enregistrez les entrées concrètes dans des tests de régression lorsqu’elles révèlent un défaut. Un rejeu utile commence avec les mêmes valeurs et un état initial connu ; la graine devient fiable parce que le test contrôle la façon dont elle est consommée.
Sources
- SystemRandomNumberGenerator — Apple.
- Améliorer l’évaluation du code en organisant les tests en plans de test — Apple.
- Random.swift — Projet Swift.
- swift-package-init-lib.md — Projet Swift.
- Annonce de Swift 6 — Swift.org.
- random(in:using:) — Apple.
- Implémenter des tests paramétrés — Apple.
- Configurer et nettoyer l’état dans vos tests — Apple.
- Exécuter les tests en série ou en parallèle — Apple.