För att spela upp randomiserade Swift-testindata på din Mac igen skickar du ett uttryckligt seedvärde till en ny generator för varje testfall. Håll generatorn, indatamappningen och dragningsordningen oförändrade och registrera sedan seedvärdet tillsammans med varje fel.
Den här genomgången bygger ett litet lokalt Swift-paket som kontrollerar en syntetisk aritmetisk beräkning. Den innehåller ett komplett exempel med Swift Testing och ett alternativ med XCTest. Alla testdata kommer från lokala beräkningar; paketet har inga externa beroenden.
Vad gör seedvärdet faktiskt repeterbart?
Det finns två separata källor till variation: värdena som ett test genererar och ordningen som testköraren schemalägger testerna i. Ett programdefinierat seedvärde styr bara den första källan när varje genererat värde kommer från motsvarande generator. Apple dokumenterar att vanliga slumpoperationer som standard använder den automatiskt seedade SystemRandomNumberGenerator. Dess dokumenterade initializer tar inget seedvärde. SystemRandomNumberGenerator
Xcode erbjuder också slumpmässig körordning för att synliggöra beroenden mellan tester. Dokumentationen för testplaner beskriver alfabetisk eller slumpmässig körordning, men dokumenterar inte något användarangivet seedvärde för att spela upp den inställningen igen. Den metod som stöds här är därför en uttryckligen seedad indatagenerator i oberoende tester. Den utlovar inte att återskapa testkörarens schemaläggning. Förbättra kodbedömningen genom att organisera tester i testplaner
Använd den här vägledningen innan du ändrar ett instabilt test:
| Vad varierar mellan körningarna? | Vad ska styras? |
|---|---|
| Genererade numeriska indata | Seedvärde, generatorimplementering, indatamappning och dragningsordning |
| Resultat efter att ett annat test har körts | Muterbart tillstånd och initiering per test |
| Resultat endast vid samtidig körning | Delade muterbara resurser och överlappande arbete |
| Indata efter en verktygskedjeuppdatering | Implementeringen av indatamappningen och registrerade konkreta indata |
Swift stöder uttryckligen anpassade generatorer via RandomNumberGenerator, vars obligatoriska operation är en muterande next() som returnerar UInt64. Swift-projektets källdokumentation anger repeterbara sekvenser för testning som ett skäl att tillhandahålla en generator som kan seedas. Random.swift
Skapa ett litet lokalt testpaket
Använd en Mac med Xcode och en tillgänglig Swift 6-verktygskedja eller senare. Kontrollera den valda kommandoradsverktygskedjan i Terminal och skapa sedan en ny katalog och initiera ett bibliotekspaket:
swift --version
mkdir SeedReplay
cd SeedReplay
swift package init --type library
Swift-projektets integrationstester testar skapandet av bibliotekspaket med swift package init --type library. Swift 6 innehåller Swift Testing direkt, så det här exemplet behöver inget paketberoende för import Testing. Swift Package Manager kör både Swift Testing-tester och XCTest-tester när sådana finns. Integrationstest för initiering av Swift-paket, Vi presenterar Swift 6
Behåll det genererade paketmanifestet och bibliotekets källkod. Ersätt den genererade exempelfilen för tester med testfilerna nedan, alla i Tests/SeedReplayTests. Den aritmetiska funktionen finns i testets stödfil för att hålla demonstrationen fristående. I ett befintligt projekt använder du i stället den verkliga funktionen som testas.
Definiera seedvärdet och kontraktet för indatagenerering
Skapa Tests/SeedReplayTests/ReplaySupport.swift med följande fullständiga innehåll:
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, +)
}
Konfigurationen accepterar ett decimalt UInt64, inklusive noll. Om TEST_SEED inte är satt väljs tre uttryckliga seedvärden; ett tomt, negativt, icke-numeriskt eller för stort värde orsakar ett fel. Ett felstavat seedvärde kan alltså inte i tysthet välja en annan körning.
Generatorn använder uttrycklig aritmetik med omslag. Varje genererad indata tar de övre tio bitarna från ett utdata, vilket ger värden från 0 till 1023. Den mappningen är avsiktligt fastlagd i källkoden. Aritmetiken förblir liten: det största förväntade resultatet är 523776. Det här är ett avgränsat syntetiskt recept för indata, utan anspråk på statistisk testtäckning.
Varför undvika Int.random(in:using:) här? API:t stöder en angiven generator och är användbart vid uppspelning med en fast verktygskedja. Apple varnar dock uttryckligen för att algoritmen som genererar värden kan ändras mellan Swift-versioner, även med en generator som producerar samma underliggande sekvens. Genom att äga den lilla mappningen i det här exemplet tar vi bort just det beroendet. Int.random(in:using:)
Kör och spela upp Swift Testing-versionen igen
Skapa 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])
}
Det parametriserade testet gör varje seedvärde till ett separat testfall. Apple dokumenterar att arguments: skickar element i en samling till testanrop och tillåter uttryck som kan kasta fel. Det gör seedvärdesvalet synligt i testmodellen. Det uttryckliga loopindexet identifierar sedan en enskild genererad indata inom det seedvärdet. Implementera parametriserade tester
Kör standardsamlingen och spela sedan upp ett seedvärde två gånger:
swift test
TEST_SEED=42 swift test
TEST_SEED=42 swift test
Dessa två seedade körningar bör generera samma numeriska indata när källkoden är oförändrad. För seedvärdet 42 är de tre första indatavärdena 759, 163 och 285. Testet kontrollerar 256 indata för det seedvärdet. Det separata stabilitetstestet använder avsiktligt alltid seedvärdet noll: det kontrollerar generatorn och mappningen oavsett vilket seedvärde som valts för det aritmetiska testet.
Det förväntade resultatet med den medföljande implementeringen är att alla påståenden godkänns. Swift var inte tillgängligt i forskningsmiljön, så Swift-filerna granskades i stället för att kompileras. En oberoende Python-beräkning kontrollerade alla 768 aritmetiska jämförelser för de tre standardvärdena och de visade inledande värdena; den kontrollen ersätter inte kompilering av Swift-paketet.
Öva på att spela upp ett avsiktligt beräkningsfel
Om du vill se diagnostikflödet byter du tillfälligt ut returuttrycket i triangularNumber(_:) mot det här märkta kodfragmentet:
// Temporary replacement inside triangularNumber(_:).
return n * n / 2
Kör TEST_SEED=42 swift test igen. Den första genererade indatan är 759. Det ändrade uttrycket ger 288040; referenssumman är 288420. Felmeddelandet identifierar seedvärde 42, fallindex 0 och indata 759. Den tidiga returen avbryter anropet efter den första registrerade avvikelsen, så diagnostiken förblir fokuserad.
Återställ return n * (n + 1) / 2 och kör samma kommando igen. Behåll seedvärdet oförändrat när du utvärderar korrigeringen. När en genererad indata har avslöjat ett användbart fel bör du också bevara den konkreta indatan i ett namngivet regressionstest; den är fortfarande begriplig även om genereringsreceptet senare ändras.
Använd samma livscykel med XCTest
För ett XCTest-baserat mål behåller du ReplaySupport.swift och använder följande fullständiga testfil i stället för Swift Testing-filen. Döp den till 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]
)
}
}
Samma Terminal-kommandon gäller. Varje anrop till generatedInputs(seed:) skapar en ny generator, så inget test förbrukar ett annat tests slumpström. Det finns inget generatortillstånd att rensa efter testet.
Om en befintlig XCTest-fixture lagrar en generator som egenskap återställer du den före varje testmetod. Apple dokumenterar instansens setup-metoder för initiering per test och klassens setup för tillstånd som delas inom klassen. En muterbar slumpström hör hemma i initiering per test när testerna ska kunna spelas upp oberoende av varandra. Att förbereda nästa tests tillstånd i teardown är opålitligt eftersom teardown inte garanteras efter en krasch. Konfigurera och rensa tillstånd i testerna
Håll Xcodes schemaläggning åtskild från uppspelning av indata
Om du vill köra det här paketet från Xcode öppnar du dess Package.swift. För ett projekt som använder en testplan väljer du Product > Scheme > Edit Test Plan, väljer relevant konfiguration och lägger till TEST_SEED med decimalvärdet 42 under Environment Variables. Kör med Product > Test och kontrollera det utskrivna seedvärdet i testutdata. Apple dokumenterar miljökonfiguration för testplaner och hur man väljer den aktiva planen innan tester körs. Förbättra kodbedömningen genom att organisera tester i testplaner
Kontrollera det utskrivna seedvärdet varje gång du växlar mellan Terminal och Xcode. En variabel som tilldelas ett enskilt Terminal-kommando är ingen inställning i Xcodes testplan. Loggraden visar vilken konfiguration testprocessen faktiskt läste.
Swift Testing kör som standard tester parallellt. Dess .serialized-egenskap kan serialisera fall i ett parametriserat test eller tester inom en svit, men den styr inte orelaterade tester. Apple dokumenterar också swift test --no-parallel för att inaktivera parallellisering globalt. Inget av dessa alternativ anger ett seedvärde för indata. Det här exemplet behöver ingen serialisering eftersom varje fall har sin egen generator och sina egna numeriska indata. Köra tester seriellt eller parallellt
Om ett test godkänns ensamt men misslyckas tillsammans med andra tester trots identiska genererade indata bör du undersöka delat tillstånd. Seriell körning kan hjälpa till att undersöka överlappande arbete, men ett seedvärde kan inte återställa ett annat tests muterbara objekt eller återskapa ett helt samtidigt körningsschema.
Vad bör du spara tillsammans med ett seedvärde som gett fel?
Spara tillräckligt med information för att återskapa indatasekvensen:
- Testnamnet och det decimala seedvärdet.
- Generatorn och versionen av mappningen, här
upper10-v1. - Fallindexet och den konkreta indatan från felmeddelandet.
- Källkodsrevisionen, Swift-versionen och den valda Xcode-versionen.
- Antalet indata och eventuella ändringar i hur slumpdragningar förbrukas.
I det här exemplet förskjuts alla efterföljande indata om du lägger till ett generatoranrop före loopen. Om du ändrar skiftmängden ändras mappningen. Om du ökar antalet indata bevaras det befintliga prefixet bara så länge generatorn och mappningen förblir oförändrade. Detta följer av kodens uppspelningskontrakt.
Behåll en liten samling fasta seedvärden för rutinmässiga körningar, prova fler uttryckliga seedvärden vid utforskning och spara konkreta regressionsindata när de avslöjar ett fel. En användbar uppspelning börjar med samma värden och ett känt initialtillstånd; seedvärdet blir tillförlitligt eftersom testet styr hur det används.
Källor
- SystemRandomNumberGenerator — Apple.
- Förbättra kodbedömningen genom att organisera tester i testplaner — Apple.
- Random.swift — Swift-projektet.
- swift-package-init-lib.md — Swift-projektet.
- Vi presenterar Swift 6 — Swift.org.
- random(in:using:) — Apple.
- Implementera parametriserade tester — Apple.
- Konfigurera och rensa tillstånd i testerna — Apple.
- Köra tester seriellt eller parallellt — Apple.