Aby odtworzyć losowe dane wejściowe testów Swift na Macu, przekaż jawne ziarno do nowego generatora dla każdego przypadku testowego. Nie zmieniaj generatora, mapowania danych wejściowych ani kolejności losowań, a przy każdym błędzie zapisuj użyte ziarno.
Ten przewodnik tworzy niewielki lokalny pakiet Swift, który sprawdza syntetyczne obliczenie arytmetyczne. Zawiera kompletny przykład dla Swift Testing oraz alternatywę z XCTest. Wszystkie dane testowe pochodzą z lokalnych obliczeń; pakiet nie ma zewnętrznych zależności.
Co właściwie zapewnia powtarzalność ziarno?
Istnieją dwa odrębne źródła zmienności: wartości generowane przez test oraz kolejność, w jakiej program uruchamiający testy je planuje. Ziarno zdefiniowane przez aplikację kontroluje tylko pierwsze z nich i tylko wtedy, gdy każda wygenerowana wartość pochodzi z odpowiadającego mu generatora. Apple dokumentuje, że standardowe operacje losowe domyślnie używają automatycznie zainicjowanego SystemRandomNumberGenerator. Udokumentowany inicjalizator tego generatora nie przyjmuje ziarna. SystemRandomNumberGenerator
Xcode oferuje także losową kolejność wykonywania, która pomaga ujawnić zależności między testami. Dokumentacja planów testów opisuje kolejność alfabetyczną lub losową, ale nie dokumentuje możliwości podania ziarna w celu odtworzenia kolejności. Obsługiwana tu metoda polega więc na użyciu jawnie inicjowanego generatora danych wejściowych w niezależnych testach. Nie gwarantuje ona odtworzenia harmonogramu uruchamiania testów. Ulepszanie oceny kodu przez organizowanie testów w plany testów
Zanim zmienisz niestabilny test, skorzystaj z poniższej wskazówki:
| Co się zmienia między uruchomieniami? | Co kontrolować |
|---|---|
| Generowane dane liczbowe | Ziarno, implementację generatora, mapowanie danych wejściowych i kolejność losowań |
| Wyniki po uruchomieniu innego testu | Zmienny stan i inicjalizację dla każdego testu |
| Wyniki tylko podczas równoczesnego wykonywania | Współdzielone zmienne zasoby i nakładające się operacje |
| Dane wejściowe po aktualizacji toolchaina | Implementację mapowania danych wejściowych i zapisane konkretne dane |
Swift jawnie obsługuje własne generatory przez RandomNumberGenerator, który wymaga mutującej operacji next() zwracającej UInt64. W dokumentacji źródłowej projektu Swift jako jeden z powodów udostępniania generatora inicjowanego ziarnem podano możliwość uzyskiwania powtarzalnych sekwencji na potrzeby testów. Random.swift
Utwórz niewielki lokalny pakiet testowy
Użyj Maca z Xcode i dostępnym toolchainem Swift 6 lub nowszym. W Terminalu sprawdź wybrany toolchain wiersza poleceń, a następnie utwórz nowy katalog i zainicjuj pakiet biblioteki:
swift --version
mkdir SeedReplay
cd SeedReplay
swift package init --type library
Testy integracyjne projektu Swift sprawdzają tworzenie pakietu biblioteki za pomocą swift package init --type library. Swift 6 zawiera Swift Testing, więc ten przykład nie wymaga zależności pakietu dla import Testing. Swift Package Manager uruchamia zarówno testy Swift Testing, jak i testy XCTest, jeśli są obecne. Test integracyjny inicjowania pakietu Swift, Zapowiedź Swift 6
Zachowaj wygenerowany manifest pakietu i kod źródłowy biblioteki. Zastąp wygenerowany przykładowy plik testowy plikami poniżej, umieszczonymi w Tests/SeedReplayTests. Funkcja arytmetyczna znajduje się w pliku pomocniczym testów, aby przykład był samowystarczalny. W istniejącym projekcie użyj zamiast niej rzeczywistej testowanej funkcji.
Zdefiniuj ziarno i reguły generowania danych wejściowych
Utwórz plik Tests/SeedReplayTests/ReplaySupport.swift o następującej pełnej zawartości:
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, +)
}
Konfiguracja akceptuje dziesiętną wartość UInt64, w tym zero. Jeśli TEST_SEED nie jest ustawione, wybierane są trzy jawne ziarna; pusta, ujemna, nienumeryczna lub przekraczająca zakres wartość powoduje błąd. Błędnie wpisane ziarno nie może więc po cichu wybrać innego przebiegu.
Generator stosuje jawne operacje arytmetyczne z zawijaniem. Każda generowana wartość wejściowa pobiera dziesięć górnych bitów jednego wyniku, co daje wartości od 0 do 1023. To mapowanie jest celowo ustalone w kodzie źródłowym. Obliczenie pozostaje niewielkie: największy oczekiwany wynik to 523776. To ograniczony przepis na syntetyczne dane wejściowe, który nie daje podstaw do twierdzeń o pokryciu statystycznym testów.
Dlaczego w tym przykładzie unikamy Int.random(in:using:)? To API obsługuje generator przekazany przez użytkownika i jest przydatne przy odtwarzaniu wyników z użyciem ustalonej wersji toolchaina. Apple wyraźnie ostrzega jednak, że algorytm generowania wartości może się zmieniać między wersjami Swifta, nawet gdy generator tworzy tę samą bazową sekwencję. Samodzielne określenie prostego mapowania na potrzeby tego przykładu usuwa tę konkretną zależność. Int.random(in:using:)
Uruchom i odtwórz wersję Swift Testing
Utwórz plik 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])
}
Test parametryzowany traktuje każde ziarno jako osobny przypadek testowy. Apple dokumentuje, że arguments: przekazuje elementy kolekcji do wywołań testu i dopuszcza wyrażenia, które mogą zgłosić błąd. Dzięki temu wybór ziarna jest widoczny w modelu testów. Jawny indeks pętli wskazuje następnie pojedyncze wygenerowane dane wejściowe dla danego ziarna. Implementowanie testów parametryzowanych
Uruchom domyślną kolekcję, a następnie dwukrotnie odtwórz przebieg z jednym ziarnem:
swift test
TEST_SEED=42 swift test
TEST_SEED=42 swift test
Przy niezmienionym kodzie te dwa uruchomienia z ziarnem powinny wygenerować te same wartości liczbowe. Dla ziarna 42 trzy pierwsze dane wejściowe to 759, 163 i 285. Test sprawdza 256 danych wejściowych dla tego ziarna. Osobny test stabilności celowo używa zawsze ziarna zero: sprawdza generator i mapowanie niezależnie od ziarna wybranego dla testu arytmetycznego.
Oczekiwany rezultat dostarczonej implementacji to przejście wszystkich asercji. W środowisku badawczym Swift nie był dostępny, dlatego pliki Swift zostały sprawdzone, ale nie skompilowane. Niezależne obliczenie w Pythonie sprawdziło wszystkie 768 porównań arytmetycznych dla trzech domyślnych ziaren oraz podane początkowe wartości; nie zastępuje ono kompilacji pakietu Swift.
Przećwicz odtwarzanie celowego błędu obliczenia
Aby zobaczyć ścieżkę diagnostyczną, tymczasowo zastąp wyrażenie zwracane w triangularNumber(_:) poniższym oznaczonym fragmentem:
// Temporary replacement inside triangularNumber(_:).
return n * n / 2
Uruchom ponownie TEST_SEED=42 swift test. Pierwsze wygenerowane dane wejściowe to 759. Zmienione wyrażenie daje 288040; wartość referencyjna to 288420. Komunikat asercji podaje ziarno 42, indeks przypadku 0 i dane wejściowe 759. Wczesny zwrot kończy to wywołanie po zapisaniu pierwszej niezgodności, dzięki czemu komunikat pozostaje konkretny.
Przywróć return n * (n + 1) / 2 i ponownie uruchom to samo polecenie. Podczas oceny poprawki nie zmieniaj ziarna. Gdy wygenerowane dane wejściowe pomogą wykryć usterkę, zachowaj je także w nazwanym teście regresyjnym; będą zrozumiałe nawet wtedy, gdy przepis generowania później się zmieni.
Zastosuj ten sam cykl pracy do XCTest
W przypadku celu opartego na XCTest zachowaj ReplaySupport.swift i zamiast pliku Swift Testing użyj poniższego kompletnego pliku testowego. Nazwij go 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]
)
}
}
Obowiązują te same polecenia Terminala. Każde wywołanie generatedInputs(seed:) tworzy nowy generator, więc żaden test nie zużywa losowej sekwencji innego testu. Po teście nie ma stanu generatora, który trzeba by wyczyścić.
Jeśli istniejąca konfiguracja XCTest przechowuje generator jako właściwość, resetuj go przed każdą metodą testową. Apple opisuje metody inicjalizacji instancji dla konfiguracji wykonywanej przed każdym testem oraz inicjalizację klasy dla stanu współdzielonego w jej obrębie. Zmienny strumień losowy należy inicjalizować dla każdego testu, gdy testy mają być niezależnie odtwarzalne. Przygotowywanie stanu kolejnego testu podczas sprzątania jest zawodne, ponieważ po awarii sprzątanie nie jest gwarantowane. Konfigurowanie i sprzątanie stanu w testach
Oddziel planowanie przez Xcode od odtwarzania danych wejściowych
Aby uruchomić ten pakiet z Xcode, otwórz jego plik Package.swift. W projekcie korzystającym z planu testów wybierz Product > Scheme > Edit Test Plan, wskaż odpowiednią konfigurację i dodaj TEST_SEED z wartością dziesiętną 42 w sekcji Environment Variables. Uruchom testy przez Product > Test i sprawdź wydrukowane ziarno w wynikach. Apple dokumentuje konfigurację zmiennych środowiskowych w planach testów oraz wybór aktywnego planu przed uruchomieniem testów. Ulepszanie oceny kodu przez organizowanie testów w plany testów
Sprawdzaj wydrukowane ziarno przy każdym przejściu między Terminalem a Xcode. Zmienna przypisana do pojedynczego polecenia w Terminalu nie jest ustawieniem planu testów Xcode. Wiersz dziennika informuje, jaką konfigurację rzeczywiście odczytał proces testowy.
Swift Testing domyślnie uruchamia testy równolegle. Atrybut .serialized może serializować przypadki w ramach testu parametryzowanego albo testy w ramach zestawu, ale nie obejmuje niezwiązanych testów. Apple dokumentuje także swift test --no-parallel, które globalnie wyłącza równoległość. Żaden z tych mechanizmów nie ustawia ziarna danych wejściowych. Ten przykład nie wymaga serializacji, ponieważ każdy przypadek ma własny generator i własne dane liczbowe. Uruchamianie testów szeregowo lub równolegle
Jeśli test przechodzi samodzielnie, ale nie przechodzi obok innych testów mimo identycznych wygenerowanych danych wejściowych, zbadaj współdzielony stan. Wykonywanie szeregowe może pomóc w diagnozowaniu nakładających się operacji, ale ziarno nie resetuje zmiennych obiektów innego testu ani nie odtwarza całego harmonogramu współbieżności.
Co zapisać razem z ziarnem błędu?
Zachowaj informacje wystarczające do odtworzenia sekwencji danych wejściowych:
- Nazwę testu i dziesiętne ziarno.
- Wersję generatora i mapowania, tutaj
upper10-v1. - Indeks przypadku i konkretne dane wejściowe z komunikatu asercji.
- Rewizję kodu źródłowego, wersję Swifta i wybraną wersję Xcode.
- Liczbę danych wejściowych oraz wszelkie zmiany w sposobie zużywania losowań.
W tym przykładzie dodanie losowania generatora przed pętlą przesuwa wszystkie kolejne dane wejściowe. Zmiana wielkości przesunięcia zmienia mapowanie. Zwiększenie liczby danych wejściowych zachowuje dotychczasowy prefiks tylko wtedy, gdy generator i mapowanie pozostają bez zmian. Wynika to z reguł odtwarzania określonych przez kod.
Do rutynowego uruchamiania zachowaj niewielką, stałą kolekcję ziaren, przy eksperymentowaniu wypróbuj dodatkowe jawne ziarna, a konkretne dane regresyjne zapisuj, gdy pomogą wykryć usterkę. Przydatne odtworzenie zaczyna się od tych samych wartości i znanego stanu początkowego; ziarno jest wiarygodne, bo test kontroluje sposób jego wykorzystania.
Źródła
- SystemRandomNumberGenerator — Apple.
- Ulepszanie oceny kodu przez organizowanie testów w plany testów — Apple.
- Random.swift — projekt Swift.
- swift-package-init-lib.md — projekt Swift.
- Zapowiedź Swift 6 — Swift.org.
- random(in:using:) — Apple.
- Implementowanie testów parametryzowanych — Apple.
- Konfigurowanie i sprzątanie stanu w testach — Apple.
- Uruchamianie testów szeregowo lub równolegle — Apple.