Чтобы воспроизвести случайные входные данные тестов Swift на вашем Mac, передайте явное значение seed в новый генератор для каждого тестового случая. Сохраняйте генератор, отображение входных данных и порядок извлечения фиксированными, затем записывайте seed вместе с каждой ошибкой.

Это руководство создаёт небольшой локальный Swift-пакет, который проверяет синтетическое арифметическое вычисление. Оно предоставляет полный пример Swift Testing и альтернативу XCTest. Все тестовые данные поступают из локальных вычислений; у пакета нет внешних зависимостей.

Что именно делает seed воспроизводимым?

Существует два отдельных источника вариации: значения, которые генерирует тест, и порядок, в котором планировщик тестов запускает тесты. Заданное приложением seed контролирует только первое, и только когда каждое генерируемое значение поступает из соответствующего генератора. Apple документирует, что обычные случайные операции по умолчанию используют автоматически инициализированный SystemRandomNumberGenerator. Его документированный инициализатор не принимает seed. SystemRandomNumberGenerator

Xcode также предлагает случайный порядок выполнения для выявления зависимостей между тестами. Документация по тест-планам описывает алфавитный или случайный порядок выполнения, но не документирует пользовательский seed для воспроизведения этой настройки. Поэтому поддерживаемый путь, используемый здесь — это явно инициализированный генератор входных данных внутри независимых тестов. Он не обещает воспроизвести планирование раннера. Улучшение оценки кода путём организации тестов в тест-планы

Используйте эту памятку перед изменением ненадёжного теста:

Что меняется между запусками? Что контролировать
Генерируемые числовые входные данные Seed, реализация генератора, отображение входных данных и порядок извлечения
Результаты после запуска другого теста Изменяемое состояние и инициализация для каждого теста
Результаты только при параллельном выполнении Общие изменяемые ресурсы и перекрывающаяся работа
Входные данные после обновления инструментария Реализация отображения входных данных и записанные конкретные входные данные

Swift явно поддерживает пользовательские генераторы через RandomNumberGenerator, чья обязательная операция — мутирующий next(), возвращающий UInt64. Документация исходного кода проекта Swift указывает воспроизводимые последовательности для тестирования как причину для предоставления инициализируемого seed генератора. Random.swift

Создайте небольшой локальный тестовый пакет

Используйте Mac с Xcode и доступным инструментарием Swift 6 или новее. Проверьте выбранный командный инструментарий в Terminal, затем создайте новый каталог и инициализируйте библиотечный пакет:

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

Интеграционные тесты проекта Swift проверяют создание библиотечного пакета с swift package init --type library. Swift 6 включает Swift Testing напрямую, поэтому этому примеру не нужна зависимость пакета для import Testing. Swift Package Manager запускает и тесты Swift Testing, и тесты XCTest, если они присутствуют. Интеграционный тест инициализации Swift-пакета, Объявление Swift 6

Сохраните сгенерированный манифест пакета и исходный код библиотеки. Замените сгенерированный образец тестового файла на приведённые ниже тестовые файлы, все внутри Tests/SeedReplayTests. Арифметическая функция находится в файле поддержки тестов, чтобы сохранить демонстрацию самодостаточной. В существующем проекте используйте реальную функцию под тестом вместо неё.

Определите контракт seed и генерации входных данных

Создайте Tests/SeedReplayTests/ReplaySupport.swift со следующим полным содержимым:

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

Конфигурация принимает десятичный UInt64, включая ноль. Незаданный TEST_SEED выбирает три явных seed; пустое, отрицательное, нечисловое или переполняющее значение выбрасывает ошибку. Опечатка в seed поэтому не может молча выбрать другой запуск.

Генератор использует явную арифметику с переполнением. Каждое генерируемое входное значение берёт верхние десять бит одного выхода, давая значения от 0 до 1023. Это отображение намеренно зафиксировано в исходном коде. Арифметика остаётся малой: наибольший ожидаемый результат — 523776. Это ограниченный синтетический рецепт входных данных, без претензий на статистическое покрытие тестами.

Зачем избегать Int.random(in:using:) здесь? Этот API поддерживает переданный генератор и полезен при воспроизведении с фиксированным инструментарием. Однако Apple явно предупреждает, что алгоритм генерации значений может меняться между версиями Swift, даже с генератором, производящим ту же базовую последовательность. Владение небольшим отображением этого примера устраняет именно эту зависимость. Int.random(in:using:)

Запустите и воспроизведите версию Swift Testing

Создайте 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])
}

Параметризованный тест делает каждый seed отдельным тестовым случаем. Apple документирует, что arguments: поставляет элементы коллекции вызовам тестов и допускает выбрасываемые выражения. Это делает выбор seed видимым в модели теста. Явный индекс цикла затем идентифицирует отдельное сгенерированное входное значение внутри этого seed. Реализация параметризованных тестов

Запустите коллекцию по умолчанию, затем воспроизведите один seed дважды:

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

Эти два запуска с seed должны сгенерировать те же числовые входные данные при неизменённом исходном коде. Для seed 42 первые три входных значения — 759, 163 и 285. Тест проверяет 256 входных значений для этого seed. Отдельный тест стабильности намеренно остаётся фиксированным на seed ноль: он проверяет генератор и отображение независимо от выбранного seed арифметического теста.

Ожидаемый результат предоставленной реализации — все утверждения проходят. Swift был недоступен в исследовательской среде, поэтому Swift-файлы проверялись визуально, а не компилировались. Независимый расчёт на Python проверил все 768 арифметических сравнений для трёх seed по умолчанию и отображённых начальных значений; эта проверка не заменяет компиляцию Swift-пакета.

Попрактикуйтесь в воспроизведении намеренной ошибки вычисления

Чтобы увидеть диагностический путь, временно замените выражение return в triangularNumber(_:) на этот помеченный фрагмент:

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

Запустите TEST_SEED=42 swift test снова. Первое сгенерированное входное значение — 759. Изменённое выражение даёт 288040; эталонная сумма — 288420. Сообщение утверждения идентифицирует seed 42, индекс случая 0 и входное значение 759. Ранний возврат останавливает этот вызов после первой зафиксированной несовпадающей пары, сохраняя диагностику сфокусированной.

Верните return n * (n + 1) / 2 и снова запустите ту же команду. Держите seed постоянным во время оценки исправления. Как только сгенерированное входное значение выявило полезный дефект, сохраните это конкретное входное значение в именованном регрессионном тесте; оно остаётся понятным даже если рецепт генерации позже изменится.

Примените тот же жизненный цикл к XCTest

Для цели на базе XCTest сохраните ReplaySupport.swift и используйте следующий полный тестовый файл вместо файла Swift Testing. Назовите его 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]
        )
    }
}

Те же команды Terminal применяются. Каждый вызов generatedInputs(seed:) создаёт новый генератор, поэтому ни один тест не потребляет случайный поток другого теста. Нет состояния генератора для очистки после теста.

Если существующий фикстура XCTest хранит генератор как свойство, сбрасывайте его перед каждым тестовым методом. Apple документирует методы настройки экземпляра для инициализации на тест и настройку класса для состояния, общего для класса. Изменяемый случайный поток принадлежит инициализации на тест, когда тесты должны воспроизводиться независимо. Подготовка состояния следующего теста в очистке ненадёжна, потому что очистка не гарантирована после аварийного завершения. Настройка и очистка состояния в ваших тестах

Держите планирование Xcode отдельным от воспроизведения входных данных

Чтобы запустить этот пакет из Xcode, откройте его Package.swift. Для проекта, использующего тест-план, выберите Product > Scheme > Edit Test Plan, выберите соответствующую конфигурацию и добавьте TEST_SEED с десятичным значением 42 в Environment Variables. Запустите с Product > Test и подтвердите напечатанный seed в выводе теста. Apple документирует конфигурацию окружения тест-плана и выбор активного плана перед запуском тестов. Улучшение оценки кода путём организации тестов в тест-планы

Проверяйте напечатанный seed всякий раз, когда переключаетесь между Terminal и Xcode. Переменная, назначенная одной команде Terminal, не является настройкой тест-плана Xcode. Строка лога говорит вам, какую конфигурацию фактически прочитал тестовый процесс.

Swift Testing запускает тесты параллельно по умолчанию. Его типаж .serialized может сериализовать случаи внутри параметризованного теста или тесты внутри набора, но не управляет несвязанными тестами. Apple также документирует swift test --no-parallel для глобального отключения параллелизации. Ни одно из средств не поставляет seed входных данных. Этот пример не нуждается в сериализации, потому что каждый случай владеет своим генератором и числовыми входными данными. Запуск тестов последовательно или параллельно

Если тест проходит в одиночку, но падает вместе с другими тестами несмотря на идентичные сгенерированные входные данные, изучите общее состояние. Последовательное выполнение может помочь расследовать перекрывающуюся работу, но seed не может сбросить изменяемые объекты другого теста или воспроизвести полное расписание параллельного выполнения.

Что следует сохранить с падающим seed?

Сохраняйте достаточно информации для реконструкции последовательности входных данных:

  • Имя теста и десятичный seed.
  • Версия генератора и отображения, здесь upper10-v1.
  • Индекс случая и конкретное входное значение из сообщения утверждения.
  • Ревизия исходного кода, версия Swift и выбранная версия Xcode.
  • Количество входных данных и любые изменения в том, как потребляются случайные извлечения.

В этом примере добавление извлечения генератора перед циклом сдвигает все последующие входные данные. Изменение величины сдвига меняет отображение. Увеличение количества входных данных сохраняет существующий префикс только пока генератор и отображение остаются неизменными. Это последствия контракта воспроизведения кода.

Держите небольшую фиксированную коллекцию seed для обычных запусков, пробуйте дополнительные явные seed при исследовании и сохраняйте конкретные регрессионные входные данные, когда они выявляют дефект. Полезное воспроизведение начинается с тех же значений и известного начального состояния; seed становится надёжным, потому что тест контролирует, как он потребляется.

Источники