Um zufällige Swift-Testeingaben auf deinem Mac erneut abzuspielen, übergib jedem Testfall einen expliziten Seed an einen neuen Generator. Behalte Generator, Eingabezuordnung und Abrufreihenfolge bei und protokolliere den Seed zusammen mit jedem Fehler.

Diese Anleitung erstellt ein kleines lokales Swift-Paket, das eine synthetische arithmetische Berechnung prüft. Sie enthält ein vollständiges Beispiel mit Swift Testing sowie eine Alternative mit XCTest. Alle Testdaten stammen aus lokalen Berechnungen; das Paket hat keine externen Abhängigkeiten.

Was macht der Seed tatsächlich reproduzierbar?

Es gibt zwei getrennte Ursachen für Abweichungen: die Werte, die ein Test generiert, und die Reihenfolge, in der der Test-Runner Tests einplant. Ein anwendungsspezifischer Seed steuert nur Ersteres, und auch nur dann, wenn jeder generierte Wert aus dem zugehörigen Generator stammt. Apple dokumentiert, dass gewöhnliche Zufallsoperationen standardmäßig den automatisch initialisierten SystemRandomNumberGenerator verwenden. Sein dokumentierter Initialisierer akzeptiert keinen Seed. SystemRandomNumberGenerator

Xcode bietet außerdem eine zufällige Ausführungsreihenfolge, um Abhängigkeiten zwischen Tests aufzudecken. Die Dokumentation zu Testplänen beschreibt eine alphabetische oder zufällige Ausführungsreihenfolge, dokumentiert für diese Einstellung jedoch keinen vom Benutzer vorgegebenen Seed zur Wiederholung. Der hier verwendete unterstützte Weg ist daher ein explizit mit Seed initialisierter Eingabegenerator in unabhängigen Tests. Er verspricht nicht, die Planung durch den Runner zu reproduzieren. Code-Bewertungen durch die Organisation von Tests in Testplänen verbessern

Nutze diese Entscheidungshilfe, bevor du einen fehlschlagenden Test änderst:

Was variiert zwischen den Durchläufen? Was kontrollieren?
Generierte numerische Eingaben Seed, Generatorimplementierung, Eingabezuordnung und Abrufreihenfolge
Ergebnisse, nachdem ein anderer Test ausgeführt wurde Veränderlicher Zustand und Initialisierung pro Test
Ergebnisse nur bei gleichzeitiger Ausführung Gemeinsam genutzte veränderliche Ressourcen und überlappende Arbeit
Eingaben nach einer Aktualisierung der Toolchain Implementierung der Eingabezuordnung und aufgezeichnete konkrete Eingaben

Swift unterstützt benutzerdefinierte Generatoren ausdrücklich über RandomNumberGenerator. Dessen erforderliche Operation ist ein veränderndes next(), das einen UInt64 zurückgibt. In der Quelldokumentation des Swift-Projekts wird die Reproduzierbarkeit von Sequenzen für Tests als Grund für einen Generator mit Seed genannt. Random.swift

Ein kleines lokales Testpaket erstellen

Verwende einen Mac mit Xcode und einer verfügbaren Swift-6- oder neueren Toolchain. Prüfe im Terminal die ausgewählte Kommandozeilen-Toolchain. Erstelle anschließend ein neues Verzeichnis und initialisiere ein Bibliothekspaket:

swift --version
mkdir SeedReplay
cd SeedReplay
swift package init --type library

Die Integrationstests des Swift-Projekts prüfen die Erstellung eines Bibliothekspakets mit swift package init --type library. Swift 6 enthält Swift Testing direkt, daher benötigt dieses Beispiel für import Testing keine Paketabhängigkeit. Swift Package Manager führt sowohl Tests mit Swift Testing als auch XCTest-Tests aus, sofern vorhanden. Integrationstest zur Initialisierung eines Swift-Pakets, Swift 6 wird angekündigt

Behalte das generierte Paketmanifest und den Bibliotheksquellcode bei. Ersetze die generierte Beispieldatei für Tests durch die folgenden Testdateien, alle in Tests/SeedReplayTests. Die arithmetische Funktion befindet sich in der Test-Hilfsdatei, damit diese Demonstration in sich abgeschlossen ist. Verwende in einem bestehenden Projekt stattdessen die tatsächlich zu testende Funktion.

Seed und Vertrag zur Eingabegenerierung definieren

Erstelle Tests/SeedReplayTests/ReplaySupport.swift mit diesem vollständigen Inhalt:

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, +)
}

Die Konfiguration akzeptiert einen dezimalen UInt64, einschließlich null. Wenn TEST_SEED nicht gesetzt ist, werden drei explizite Seeds ausgewählt. Ein leerer, negativer, nicht numerischer oder überlaufender Wert löst einen Fehler aus. Ein falsch eingegebener Seed kann daher nicht unbemerkt einen anderen Durchlauf auswählen.

Der Generator verwendet explizite Arithmetik mit Überlauf. Jede generierte Eingabe nimmt die oberen zehn Bits eines Ausgabewerts und ergibt damit Werte von 0 bis 1023. Diese Zuordnung ist bewusst fest im Quellcode definiert. Die Arithmetik bleibt einfach: Das größte erwartete Ergebnis ist 523776. Dies ist ein begrenztes Rezept für synthetische Eingaben, ohne Anspruch auf statistische Testabdeckung.

Warum hier Int.random(in:using:) vermeiden? Diese API unterstützt einen übergebenen Generator und ist nützlich, wenn mit einer festen Toolchain eine Wiederholung erfolgen soll. Apple warnt jedoch ausdrücklich, dass sich der Algorithmus zur Wertgenerierung zwischen Swift-Versionen ändern kann, selbst wenn ein Generator dieselbe zugrunde liegende Sequenz erzeugt. Wenn die kleine Zuordnung in diesem Beispiel selbst implementiert wird, entfällt diese spezielle Abhängigkeit. Int.random(in:using:)

Die Version mit Swift Testing ausführen und wiederholen

Erstelle 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])
}

Der parametrisierte Test macht jeden Seed zu einem eigenen Testfall. Apple dokumentiert, dass arguments: Sammlungselemente für Testaufrufe bereitstellt und werfende Ausdrücke zulässt. So bleibt die Seed-Auswahl im Testmodell sichtbar. Der explizite Schleifenindex identifiziert dann eine einzelne generierte Eingabe innerhalb dieses Seeds. Parametrisierte Tests implementieren

Führe zuerst die Standardauswahl aus und wiederhole dann denselben Seed zweimal:

swift test
TEST_SEED=42 swift test
TEST_SEED=42 swift test

Diese beiden Durchläufe mit Seed sollten bei unverändertem Quellcode dieselben numerischen Eingaben erzeugen. Für den Seed 42 lauten die ersten drei Eingaben 759, 163 und 285. Der Test prüft für diesen Seed 256 Eingaben. Der separate Stabilitätstest bleibt bewusst auf Seed null festgelegt: Er prüft Generator und Zuordnung unabhängig vom ausgewählten Seed des Arithmetiktests.

Beim bereitgestellten Beispiel sollten alle Assertions erfolgreich sein. Swift war in der Rechercheumgebung nicht verfügbar, daher wurden die Swift-Dateien geprüft, aber nicht kompiliert. Eine unabhängige Python-Berechnung prüfte alle 768 arithmetischen Vergleiche für die drei Standard-Seeds sowie die angezeigten Anfangswerte. Diese Prüfung ersetzt nicht das Kompilieren des Swift-Pakets.

Das Wiederholen eines absichtlichen Berechnungsfehlers ausprobieren

Um den Diagnoseweg zu sehen, ersetze vorübergehend den Return-Ausdruck in triangularNumber(_:) durch dieses gekennzeichnete Fragment:

// Temporary replacement inside triangularNumber(_:).
return n * n / 2

Führe TEST_SEED=42 swift test erneut aus. Die erste generierte Eingabe ist 759. Der geänderte Ausdruck ergibt 288040; die Referenzsumme ist 288420. Die Assertion-Meldung nennt Seed 42, Fallindex 0 und Eingabe 759. Die vorzeitige Rückgabe beendet diesen Aufruf nach der ersten erfassten Abweichung und hält so die Diagnose gezielt.

Stelle return n * (n + 1) / 2 wieder her und führe denselben Befehl erneut aus. Behalte bei der Bewertung der Korrektur denselben Seed bei. Wenn eine generierte Eingabe einen nützlichen Fehler aufgedeckt hat, halte diese konkrete Eingabe zusätzlich in einem benannten Regressionstest fest. Sie bleibt verständlich, auch wenn sich das Generierungsrezept später ändert.

Den gleichen Lebenszyklus auf XCTest anwenden

Verwende für ein XCTest-basiertes Target weiterhin ReplaySupport.swift und statt der Swift-Testing-Datei die folgende vollständige Testdatei. Nenne sie 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]
        )
    }
}

Es gelten dieselben Terminal-Befehle. Jeder Aufruf von generatedInputs(seed:) erstellt einen neuen Generator, sodass kein Test den Zufallsstrom eines anderen Tests verbraucht. Nach dem Test muss kein Generatorzustand bereinigt werden.

Wenn eine bestehende XCTest-Fixture einen Generator als Property speichert, setze ihn vor jeder Testmethode zurück. Apple dokumentiert Instanz-Setup-Methoden für die Initialisierung pro Test und Klassen-Setup für den innerhalb einer Klasse gemeinsam genutzten Zustand. Ein veränderlicher Zufallsstrom gehört zur Initialisierung pro Test, wenn Tests unabhängig voneinander wiederholt werden sollen. Den Zustand für den nächsten Test in der Teardown vorzubereiten, ist unzuverlässig, weil nach einem Absturz keine Teardown garantiert ist. Zustand in Tests einrichten und aufräumen

Xcode-Testplanung von der Wiederholung der Eingaben getrennt halten

Um dieses Paket in Xcode auszuführen, öffne dessen Package.swift. Wähle bei einem Projekt mit Testplan Product > Scheme > Edit Test Plan, dann die passende Konfiguration, und füge unter Environment Variables TEST_SEED mit dem Dezimalwert 42 hinzu. Führe den Test mit Product > Test aus und prüfe den ausgegebenen Seed. Apple dokumentiert die Konfiguration der Testplan-Umgebung und die Auswahl des aktiven Plans vor der Testausführung. Code-Bewertungen durch die Organisation von Tests in Testplänen verbessern

Prüfe den ausgegebenen Seed immer, wenn du zwischen Terminal und Xcode wechselst. Eine Variable, die nur für einen einzelnen Terminal-Befehl gesetzt wurde, ist keine Xcode-Testplaneinstellung. Die Protokollzeile zeigt, welche Konfiguration der Testprozess tatsächlich gelesen hat.

Swift Testing führt Tests standardmäßig parallel aus. Das Trait .serialized kann Fälle innerhalb eines parametrisierten Tests oder Tests innerhalb einer Suite nacheinander ausführen, steuert jedoch keine unabhängigen Tests. Apple dokumentiert außerdem swift test --no-parallel, um Parallelisierung global zu deaktivieren. Keine dieser Einstellungen legt einen Seed für Eingaben fest. Dieses Beispiel braucht keine Serialisierung, weil jeder Fall seinen eigenen Generator und seine eigenen numerischen Eingaben besitzt. Tests seriell oder parallel ausführen

Wenn ein Test allein besteht, aber zusammen mit anderen Tests trotz identischer generierter Eingaben fehlschlägt, untersuche den gemeinsam genutzten Zustand. Eine serielle Ausführung kann bei der Untersuchung überlappender Arbeit helfen. Ein Seed kann jedoch weder veränderliche Objekte eines anderen Tests zurücksetzen noch einen vollständigen Ablauf gleichzeitiger Ausführung reproduzieren.

Was solltest du zusammen mit einem fehlschlagenden Seed aufbewahren?

Bewahre genügend Informationen auf, um die Eingabesequenz zu rekonstruieren:

  • Den Testnamen und den dezimalen Seed.
  • Die Version von Generator und Zuordnung, hier upper10-v1.
  • Den Fallindex und die konkrete Eingabe aus der Assertion-Meldung.
  • Die Quellcode-Revision sowie die Swift- und die ausgewählte Xcode-Version.
  • Die Anzahl der Eingaben und alle Änderungen daran, wie Zufallswerte abgerufen werden.

In diesem Beispiel verschiebt ein zusätzlicher Generatoraufruf vor der Schleife jede nachfolgende Eingabe. Eine Änderung des Verschiebungswerts ändert die Zuordnung. Eine Erhöhung der Anzahl der Eingaben lässt das vorhandene Präfix nur dann unverändert, wenn Generator und Zuordnung gleich bleiben. Das sind Folgen des Wiederholungsvertrags im Code.

Behalte für routinemäßige Durchläufe eine kleine feste Seed-Auswahl bei, probiere beim Erkunden zusätzliche explizite Seeds aus und speichere konkrete Regressionseingaben, wenn sie einen Fehler aufdecken. Eine nützliche Wiederholung beginnt mit denselben Werten und einem bekannten Anfangszustand. Der Seed wird zuverlässig, weil der Test kontrolliert, wie er verwendet wird.

Quellen