Mac’inizde rastgele Swift test girdilerini yeniden çalıştırmak için her test durumu için yeni bir üreteç oluşturup ona açık bir tohum geçirin. Üreteci, girdi eşlemesini ve çekim sırasını sabit tutun, ardından her hatayla birlikte tohumu kaydedin.

Bu kılavuz, sentetik bir aritmetik hesabı denetleyen küçük bir yerel Swift paketi oluşturur. Eksiksiz bir Swift Testing örneği ve bir XCTest alternatifi sunar. Tüm test verileri yerel hesaplamalardan gelir; paketin harici bağımlılığı yoktur.

Tohum tam olarak neyi tekrarlanabilir kılar?

İki ayrı değişkenlik kaynağı vardır: testin oluşturduğu değerler ve test çalıştırıcısının testleri zamanlama sırası. Uygulama tarafından tanımlanan bir tohum, yalnızca oluşturulan her değer ilgili üreteçten geldiğinde ilkini denetler. Apple, sıradan rastgele işlemlerin varsayılan olarak otomatik tohumlanan SystemRandomNumberGenerator kullandığını belgeliyor. Belgelenen başlatıcısı tohum kabul etmez. SystemRandomNumberGenerator

Xcode, testler arasındaki bağımlılıkları ortaya çıkarmak için rastgele yürütme sırası da sunar. Test planı belgeleri alfabetik veya rastgele yürütme sırasını açıklıyor, ancak bu ayar için kullanıcının belirlediği bir yeniden oynatma tohumu belgelemiyor. Bu nedenle burada kullanılan desteklenen yol, bağımsız testlerin içinde açıkça tohumlanmış bir girdi üreteci kullanmaktır. Bu yol, çalıştırıcının zamanlama sırasını yeniden üretmeyi garanti etmez. Testleri test planları halinde düzenleyerek kod değerlendirmesini iyileştirme

Kararsız bir testi değiştirmeden önce bu karar tablosunu kullanın:

Çalıştırmalar arasında ne değişiyor? Neyi denetlemeli?
Oluşturulan sayısal girdiler Tohum, üreteç uygulaması, girdi eşlemesi ve çekim sırası
Başka bir test çalıştıktan sonraki sonuçlar Değiştirilebilir durum ve test başına başlatma
Yalnızca eşzamanlı yürütme sırasındaki sonuçlar Paylaşılan değiştirilebilir kaynaklar ve çakışan işler
Araç zinciri güncellemesinden sonraki girdiler Girdi eşleme uygulaması ve kaydedilmiş somut girdiler

Swift, RandomNumberGenerator aracılığıyla özel üreteçleri açıkça destekler. Bu protokolün zorunlu işlemi, UInt64 döndüren mutating bir next() işlemidir. Swift projesinin kaynak belgelerinde, tekrarlanabilir test dizileri oluşturmanın tohumlanabilir bir üreteç sağlama nedenlerinden biri olduğu belirtiliyor. Random.swift

Küçük bir yerel test paketi oluşturun

Xcode yüklü ve Swift 6 veya sonraki bir araç zinciri kullanılabilir durumda olan bir Mac kullanın. Terminal’de seçili komut satırı araç zincirini denetleyin, ardından yeni bir dizin oluşturup bir kitaplık paketi başlatın:

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

Swift projesinin tümleştirme testleri, swift package init --type library komutuyla kitaplık paketi oluşturulmasını sınar. Swift 6, Swift Testing’i doğrudan içerir; bu nedenle bu örnekte import Testing için paket bağımlılığı gerekmez. Swift Package Manager, mevcut olduklarında hem Swift Testing testlerini hem de XCTest testlerini çalıştırır. Swift paketi başlatma tümleştirme testi, Swift 6’yı duyuruyoruz

Oluşturulan paket manifestini ve kitaplık kaynak dosyasını koruyun. Oluşturulan örnek test dosyasını aşağıdaki test dosyalarıyla değiştirin; hepsi Tests/SeedReplayTests içinde yer almalı. Bu örneği kendi kendine yeterli tutmak için aritmetik işlevi test destek dosyasında bulunuyor. Mevcut bir projede bunun yerine test edilen gerçek işlevi kullanın.

Tohum ve girdi oluşturma sözleşmesini tanımlayın

Tests/SeedReplayTests/ReplaySupport.swift dosyasını oluşturup şu içeriğin tamamını ekleyin:

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

Yapılandırma, sıfır da dahil olmak üzere ondalık bir UInt64 kabul eder. Ayarlanmamış bir TEST_SEED üç açık tohum seçer; boş, negatif, sayısal olmayan veya sınırı aşan bir değer hata fırlatır. Böylece yanlış yazılmış bir tohum sessizce farklı bir çalıştırma seçemez.

Üreteç, açık sarma aritmetiği kullanır. Oluşturulan her girdi, bir çıktının en üstteki on bitini alarak 0 ile 1023 arasında değerler verir. Bu eşleme kaynak kodda bilerek sabit tutulmuştur. Aritmetik küçük kalır: beklenen en büyük sonuç 523776 değeridir. Bu, istatistiksel test kapsamı hakkında bir iddia içermeyen, sınırları belirli sentetik bir girdi tarifidir.

Burada Int.random(in:using:) kullanmaktan neden kaçınıyoruz? Bu API, sağlanan bir üreteci destekler ve sabit bir araç zinciriyle yeniden oynatma için yararlıdır. Ancak Apple, aynı temel diziyi üreten bir üreteç kullanılsa bile değer oluşturma algoritmasının Swift sürümleri arasında değişebileceği konusunda açıkça uyarıyor. Bu örneğin küçük eşlemesini kendimiz tanımlamak, bu özel bağımlılığı ortadan kaldırır. Int.random(in:using:)

Swift Testing sürümünü çalıştırıp yeniden oynatın

Tests/SeedReplayTests/SeedReplayTests.swift dosyasını oluşturun:

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

Parametreli test, her tohumu ayrı bir test durumu olarak ele alır. Apple, arguments: parametresinin koleksiyon öğelerini test çağrılarına sağladığını ve hata fırlatan ifadelere izin verdiğini belgeliyor. Bu sayede tohum seçimi test modelinde görünür olur. Açık döngü indeksi de söz konusu tohum altındaki tekil bir girdiyi belirler. Parametreli testleri uygulama

Varsayılan koleksiyonu çalıştırıp ardından bir tohumu iki kez yeniden oynatın:

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

Kaynak kod değişmediğinde, bu iki tohumlanmış çalıştırmanın aynı sayısal girdileri üretmesi gerekir. 42 tohumu için ilk üç girdi 759, 163 ve 285 değerleridir. Test bu tohum için 256 girdiyi denetler. Ayrı kararlılık testi bilerek sıfır tohumunda sabit kalır: seçili aritmetik testi tohumu ne olursa olsun üreteci ve eşlemeyi denetler.

Sağlanan uygulamada beklenen sonuç, tüm doğrulamaların başarılı olmasıdır. Araştırma ortamında Swift kullanılamadığından Swift dosyaları derlenmek yerine incelendi. Bağımsız bir Python hesabı, varsayılan üç tohum için 768 aritmetik karşılaştırmanın ve gösterilen ilk değerlerin tümünü denetledi; bu denetim Swift paketini derlemenin yerini tutmaz.

Bilerek oluşturulan bir hesaplama hatasını yeniden oynama alıştırması yapın

Tanılama yolunu görmek için triangularNumber(_:) içindeki dönüş ifadesini geçici olarak aşağıdaki etiketli parçayla değiştirin:

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

TEST_SEED=42 swift test komutunu yeniden çalıştırın. İlk oluşturulan girdi 759 değeridir. Değiştirilmiş ifade 288040 sonucunu verir; beklenen toplam ise 288420 değeridir. Doğrulama iletisi 42 tohumunu, 0 durum indeksini ve 759 girdisini belirtir. Erken dönüş, ilk kaydedilen uyuşmazlıktan sonra bu çağrıyı durdurarak tanılamayı odaklı tutar.

return n * (n + 1) / 2 dosyasını geri yükleyip aynı komutu yeniden çalıştırın. Düzeltmeyi değerlendirirken tohumu sabit tutun. Oluşturulan bir girdi yararlı bir hatayı ortaya çıkardıktan sonra, bu somut girdiyi adlandırılmış bir gerileme testinde de saklayın; oluşturma tarifi daha sonra değişse bile bu girdi anlaşılır kalır.

Aynı yaşam döngüsünü XCTest’e uygulayın

XCTest tabanlı bir hedefte ReplaySupport.swift dosyasını koruyun ve Swift Testing dosyasının yerine aşağıdaki eksiksiz test dosyasını kullanın. Dosyayı Tests/SeedReplayTests/SeedReplayXCTests.swift olarak adlandırın:

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

Aynı Terminal komutları geçerlidir. Her generatedInputs(seed:) çağrısı yeni bir üreteç oluşturur; böylece hiçbir test başka bir testin rastgele sayı akışını tüketmez. Testten sonra temizlenmesi gereken bir üreteç durumu yoktur.

Mevcut bir XCTest fixture’ı üreteci özellik olarak saklıyorsa, her test yönteminden önce sıfırlayın. Apple, test başına başlatma için örnek kurulum yöntemlerini ve sınıf genelinde paylaşılan durum için sınıf kurulumunu belgeliyor. Testlerin bağımsız biçimde yeniden oynatılması gerekiyorsa değiştirilebilir rastgele sayı akışı test başına başlatma sırasında kurulmalıdır. Sonraki testin durumunu teardown sırasında hazırlamak güvenilir değildir; çünkü çökme sonrasında teardown’ın çalışacağı garanti edilmez. Testlerinizde Durumu Kurma ve Temizleme

Xcode zamanlamasını girdi yeniden oynatmasından ayrı tutun

Bu paketi Xcode’dan çalıştırmak için Package.swift dosyasını açın. Test planı kullanan bir projede Product > Scheme > Edit Test Plan yolunu izleyin, ilgili yapılandırmayı seçin ve Environment Variables bölümüne ondalık 42 değeriyle TEST_SEED ekleyin. Product > Test ile çalıştırın ve test çıktısında yazdırılan tohumu doğrulayın. Apple, test planı ortamı yapılandırmasını ve testleri çalıştırmadan önce etkin planı seçmeyi belgeliyor. Testleri test planları halinde düzenleyerek kod değerlendirmesini iyileştirme

Terminal ile Xcode arasında geçiş yaptığınızda yazdırılan tohumu doğrulayın. Tek bir Terminal komutuna atanan değişken, Xcode test planı ayarı değildir. Günlük satırı, test sürecinin gerçekte hangi yapılandırmayı okuduğunu gösterir.

Swift Testing varsayılan olarak testleri paralel çalıştırır. .serialized özelliği, parametreli bir test içindeki durumları veya bir takım içindeki testleri sırayla çalıştırabilir; ancak ilgisiz testleri yönetmez. Apple ayrıca paralelliği genel olarak devre dışı bırakmak için swift test --no-parallel komutunu belgeliyor. Bu denetimlerin hiçbiri girdi tohumu sağlamaz. Her durum kendi üretecine ve sayısal girdilerine sahip olduğundan bu örneğin serileştirmeye ihtiyacı yoktur. Testleri sırayla veya paralel çalıştırma

Bir test tek başına geçiyor, ancak oluşturulan girdiler aynı olmasına rağmen başka testlerle birlikte başarısız oluyorsa paylaşılan durumu inceleyin. Sırayla yürütme, çakışan işleri araştırmaya yardımcı olabilir; ancak bir tohum başka bir testin değiştirilebilir nesnelerini sıfırlayamaz veya eşzamanlı bir zamanlama dizisinin tamamını yeniden üretemez.

Başarısız bir tohumla birlikte neleri saklamalısınız?

Girdi dizisini yeniden oluşturmak için yeterli bilgiyi koruyun:

  • Test adı ve ondalık tohum.
  • Bu örnekte upper10-v1 olan üreteç ve eşleme sürümü.
  • Doğrulama iletisindeki durum indeksi ve somut girdi.
  • Kaynak revizyonu, Swift sürümü ve seçili Xcode sürümü.
  • Girdi sayısı ve rastgele sayı çekimlerinin tüketilme biçimindeki tüm değişiklikler.

Bu örnekte döngüden önce üreteçten bir değer daha çekmek, sonraki tüm girdileri kaydırır. Kaydırma miktarını değiştirmek eşlemeyi değiştirir. Girdi sayısını artırmak, üreteç ve eşleme değişmediği sürece mevcut öneki korur. Bunlar kodun yeniden oynatma sözleşmesinin sonuçlarıdır.

Rutin çalıştırmalar için küçük, sabit bir tohum koleksiyonu bulundurun; keşif yaparken başka açık tohumlar deneyin ve bir hata ortaya çıkardıklarında somut gerileme girdilerini kaydedin. Yararlı bir yeniden oynatma aynı değerlerle ve bilinen bir başlangıç durumuyla başlar; tohum, tüketilme biçimi test tarafından denetlendiği için güvenilir hale gelir.

Kaynaklar