Aby testy XCTest przechodziły zarówno pojedynczo, jak i w całym zestawie, zapewnij każdemu testowi własny mutowalny fixture i wstrzyknij go do testowanego kodu. Jeśli singleton musi pozostać współdzielony, resetuj go przed każdym testem i zapobiegaj nakładaniu się dostępu przez cały czas trwania testu.

Ten poradnik opisuje jedno zadanie programistyczne na Macu: naprawę testów obliczania układu w Xcode, które przechodzą osobno, ale zawodzą uruchomione razem. Przykład korzysta wyłącznie ze sztucznych szerokości wyrażonych liczbami całkowitymi i lokalnych obliczeń. Nie wymaga okna aplikacji, mocka usługi, przechowywania plików ani połączenia sieciowego.

Dlaczego test może przejść osobno, a zawieść w zestawie?

Test uruchomiony w izolacji może napotkać początkowy stan singletonu. W zestawie wcześniejszy test mógł już go zmienić. Nowa referencja do tego singletonu nadal wskazuje ten sam mutowalny obiekt.

Swift rozróżnia właściwości instancji i właściwości typu: każda instancja ma własne właściwości instancji, natomiast właściwość typu ma jedną współdzieloną kopię. Właściwości typu przechowywanej inicjalizują się przy pierwszym dostępie i tylko raz. Dlatego przypisanie SpacingCalculator.shared do właściwości testu nie tworzy nowego kalkulatora. Zobacz dokumentację właściwości projektu Swift.

Rozważmy sztuczny wiersz zawierający elementy o szerokościach [10, 20, 30]. Jego całkowita szerokość to suma szerokości elementów i odstępów między nimi. Przy domyślnym odstępie 2 wynik wynosi 64; przy odstępie 8 wynosi 76.

Załóżmy, że jeden test sprawdza domyślną szerokość bez konfigurowania kalkulatora, a drugi zmienia współdzielony odstęp na 8. Poniższa tabela opisuje wykonanie sekwencyjne, rozpoczynając każdy wiersz od nowego kalkulatora z odstępem 2. Są to przykładowe obliczenia, a nie zarejestrowane wyniki testów Xcode.

Wykonanie Współdzielony kalkulator bez resetowania Nowy kalkulator dla każdego testu
Sam test domyślny 64: przechodzi 64: przechodzi
Sam test z dużym odstępem 76: przechodzi 76: przechodzi
Domyślny, potem z dużym odstępem Oba przechodzą Oba przechodzą
Z dużym odstępem, potem domyślny Domyślny otrzymuje 76: zawodzi Oba przechodzą

To daje przydatną wskazówkę diagnostyczną: sprawdź, który test zmienia stan używany przez zawodną asercję. Zmiana nazw testów w celu uzyskania korzystnej kolejności pozostawia zależność bez zmian.

Która metoda cyklu życia XCTest powinna zarządzać fixture’em?

Użyj instancyjnej metody konfiguracji dla mutowalnego stanu potrzebnego każdemu testowi. Apple dokumentuje, że instancyjna konfiguracja jest wykonywana przed każdą metodą testową, podczas gdy konfiguracja na poziomie klasy uruchamia się raz przed klasą testową. Mutowalny fixture utworzony w konfiguracji na poziomie klasy nie otrzymuje więc nowego stanu początkowego między metodami. Zobacz Konfigurowanie i czyszczenie stanu w testach.

Wybierz metodę konfiguracji odpowiednią do wykonywanej pracy: synchroniczne, niewyrzucające błędów tworzenie fixture’u pasuje do setUp(); tworzenie mogące zgłosić błąd — do setUpWithError(); asynchroniczne przygotowanie — do setUp() async throws. Użyj metody czyszczenia, aby zakończyć porządkowanie po bieżącym teście. Apple wyraźnie odradza poleganie na czyszczeniu przy przygotowywaniu kolejnych testów, ponieważ awaria może uniemożliwić jego uruchomienie. Te granice opisuje ta sama dokumentacja cyklu życia.

Wybór fixture’u zależy zarówno od tego, kto jest jego właścicielem, jak i od zakresu wykonania:

Sytuacja Odpowiednie podejście Warunek do sprawdzenia
Testy mogą utworzyć zależność Utwórz i wstrzyknij instancję dla każdego testu Odbiorca musi korzystać z wstrzykniętej instancji
Testy muszą sprawdzać rzeczywisty singleton Ustal pełny stan początkowy przed każdym testem Żaden nakładający się test nie może zmieniać tego singletonu
Współdzielona konfiguracja jest niezmienna Użyj ponownie niezmiennej wartości Referencjonowane obiekty również muszą pozostać niezmienione
Test uruchamia pracę asynchroniczną Poczekaj na jej zakończenie przed końcem testu Żadna niezakończona praca nie może później modyfikować fixture’u

Pierwsza opcja jest najprostsza dla tego kalkulatora. Kod produkcyjny może zachować współdzieloną instancję domyślną, a testy mogą jawnie wybierać niezależne instancje.

Utwórz niezależny fixture XCTest

Użyj Maca z Xcode i istniejącego celu testów jednostkowych macOS. Dodaj poniższy kompletny przykład jako jeden plik Swift w tym celu testowym. Definiuje własny kalkulator, odbiorcę i testy, więc nie wymaga importu modułu aplikacji. Użyj toolchaina Swift 6 z domyślną izolacją aktorową celu ustawioną na nonisolated.

Kalkulator jest aktorem, aby jawnie określić granicę współbieżności jego mutowalnego stanu. Aktory Swifta ograniczają dostęp do swojego mutowalnego stanu, a wywołania z zewnątrz używają await. Zasady te opisuje rozdział o współbieżności projektu Swift. Niezależna własność fixture’u jest konieczna także w przypadku aktora.

import XCTest

actor SpacingCalculator {
    static let shared = SpacingCalculator()

    private var gap = 2

    func setGap(_ value: Int) {
        precondition(value >= 0)
        gap = value
    }

    func width(of itemWidths: [Int]) -> Int {
        precondition(itemWidths.allSatisfy { $0 >= 0 })
        let gapCount = max(itemWidths.count - 1, 0)
        return itemWidths.reduce(0, +) + gapCount * gap
    }
}

struct RowLayout {
    let calculator: SpacingCalculator

    init(calculator: SpacingCalculator = .shared) {
        self.calculator = calculator
    }

    func width(of itemWidths: [Int]) async -> Int {
        await calculator.width(of: itemWidths)
    }
}

final class RowLayoutTests: XCTestCase {
    private var calculator: SpacingCalculator?

    override func setUp() {
        super.setUp()
        calculator = SpacingCalculator()
    }

    override func tearDown() {
        calculator = nil
        super.tearDown()
    }

    func testDefaultGap() async throws {
        let fixture = try XCTUnwrap(calculator)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [10, 20, 30])

        XCTAssertEqual(actual, 64)
    }

    func testWideGap() async throws {
        let fixture = try XCTUnwrap(calculator)
        await fixture.setGap(8)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [10, 20, 30])

        XCTAssertEqual(actual, 76)
    }

    func testEmptyRow() async throws {
        let fixture = try XCTUnwrap(calculator)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [])

        XCTAssertEqual(actual, 0)
    }

    func testSingleItem() async throws {
        let fixture = try XCTUnwrap(calculator)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [10])

        XCTAssertEqual(actual, 10)
    }
}

Kluczowa linia to calculator = SpacingCalculator(). Każda konfiguracja tworzy niezależnego aktora z odstępem 2. Następnie każdy test przekazuje tego aktora do RowLayout. Wywołanie w tych testach RowLayout() wybrałoby .shared i ominęło fixture.

Przykład celowo umożliwia bezpośrednie tworzenie instancji. Jeśli istniejący singleton ma prywatny inicjalizator, dodaj wewnętrzną ścieżkę tworzenia albo wydziel jego mutowalny stan obliczeń do osobnej, możliwej do utworzenia zależności. Wewnętrzny inicjalizator można udostępnić celowi testowemu za pomocą przyjętego w module aplikacji mechanizmu importu testowalnego; globalna możliwość podmieniania referencji singletonu nie jest potrzebna.

Obliczenia definiują też dwa przydatne przypadki brzegowe: pusty wiersz ma szerokość 0, a pojedynczy element nie ma odstępu między elementami. Przykład przyjmuje nieujemne liczby całkowite na tyle małe, by uniknąć przepełnienia. Wartości ujemne naruszają warunki wstępne i przerywają wykonanie zamiast powodować niepowodzenie asercji.

Oczekiwane obliczenia oraz obie kolejności dwóch testów sprawdzono lokalnie za pomocą deterministycznych obliczeń w Pythonie. Przykłady Swift zostały przejrzane, ale nie skompilowano ich ani nie uruchomiono, ponieważ to środowisko badawcze nie ma toolchaina Swift. Uruchom je w Xcode, aby sprawdzić kompilację i wykonanie w swoim celu.

Kiedy resetowanie singletonu wystarczy?

Resetowanie jest możliwym rozwiązaniem przejściowym, gdy testy rzeczywiście muszą korzystać ze współdzielonej instancji i można przywrócić cały jej stan widoczny dla testów. Ustal stan początkowy przed każdym testem, uwzględniając wartości, które test uznaje za domyślne. Resetowanie wyłącznie właściwości zmienionej przez aktualnie zawodny test może pozostawić inne, nieodkryte zależności.

W przypadku tego kalkulatora asynchroniczna konfiguracja mogłaby wybrać SpacingCalculator.shared i przed każdą metodą wywołać setGap(2), oczekując na zakończenie. Ustanawia to podstawowy stan obliczeń tylko pod warunkiem, że żaden nakładający się wywołujący nie zmieni odstępu przed asercją. Jeśli fixture zyska później buforowane wyniki lub dodatkową konfigurację, operacja ustalająca stan początkowy musi objąć również te wartości.

Aktor nie daje jednemu testowi wyłącznej własności na czas kilku wywołań oczekujących. Na przykład test A może ustawić współdzielony odstęp na 2, test B może ustawić go na 8, a następnie test A może obliczyć szerokość przy użyciu 8. Każda operacja aktora może wykonać się poprawnie, a mimo to założenie całego testu może zawieść. Wynika to z udokumentowanych przez Swifta zasad wykonywania aktorów i zawieszania.

Zanim wybierzesz izolację przez resetowanie, ustal, które testy i zadania działające w tle mogą uzyskać dostęp do współdzielonego obiektu. Jeśli nie możesz zapewnić wyłącznego dostępu podczas przygotowania, wykonania i czyszczenia, zamiast tego wstrzyknij niezależny stan. Późniejsze przywrócenie migawki nie zapobiegnie temu, by inny wywołujący zauważył tymczasowe zmiany podczas testu.

Jak przenieść fixture do Swift Testing?

Swift Testing inicjalizuje odrębną instancję zestawu dla każdej instancyjnej funkcji testowej. Właściwość instancji tworząca kalkulator zapewnia więc ten sam wzorzec własności bez nadpisywania metod konfiguracji XCTest. Właściwość zainicjalizowana przez .shared nadal wskazywałaby singleton. Apple opisuje cykl życia instancji w artykule Porządkowanie funkcji testowych za pomocą typów zestawów.

Gdy wcześniejsze definicje SpacingCalculator i RowLayout znajdują się raz w tym samym celu testowym, poniższy kod stanowi kompletny dodatkowy zestaw Swift Testing. Wymaga toolchaina zawierającego moduł Testing, na przykład Xcode 16 lub nowszego. Domyślna izolacja aktorowa również powinna mieć wartość nonisolated.

import Testing

struct RowLayoutSwiftTests {
    let calculator = SpacingCalculator()

    @Test func defaultGap() async {
        let layout = RowLayout(calculator: calculator)
        let actual = await layout.width(of: [10, 20, 30])
        #expect(actual == 64)
    }

    @Test func wideGap() async {
        await calculator.setGap(8)
        let layout = RowLayout(calculator: calculator)
        let actual = await layout.width(of: [10, 20, 30])
        #expect(actual == 76)
    }
}

W przypadku bardziej złożonego przygotowania Swift Testing obsługuje inicjalizatory zestawu, w tym asynchroniczne i zgłaszające błędy. Artykuł Apple Migracja testu z XCTest opisuje to rozwiązanie zastępujące metody konfiguracji XCTest. Ten fixture nie wymaga jawnego czyszczenia, ponieważ obejmuje wyłącznie lokalny stan obliczeń i nie uruchamia zadań w tle.

Swift Testing domyślnie uruchamia testy równolegle. Zastosowanie .serialized do zestawu uruchamia zawarte w nim testy i zagnieżdżone zestawy sekwencyjnie, ale nie zapobiega równoległemu uruchomieniu niezwiązanych testów. Zastosowanie tej cechy do zwykłej, nieparametryzowanej funkcji testowej nie przynosi efektu. Te ograniczenia opisano wprost w artykule Uruchamianie testów sekwencyjnie lub równolegle.

W związku z tym umieszczenie dwóch zestawów zależnych od singletonu pod osobnymi adnotacjami .serialized nie zapewnia wzajemnego wykluczania. Wspólny serializowany zestaw nadrzędny może koordynować elementy potomne, pod warunkiem że każdy konkurujący użytkownik należy do tego zakresu. Nowe fixture’y pozwalają uniknąć tej potrzeby koordynacji w testach obliczeń.

Sprawdź wyniki pojedynczych testów i całego zestawu w Xcode

Podczas całej weryfikacji używaj tego samego schematu, planu testów i miejsca docelowego na Macu. Apple opisuje uruchamianie pojedynczych metod, zestawów i aktywnego planu testów w artykule Uruchamianie testów i interpretowanie wyników.

  1. Uruchom każdą z czterech metod XCTest osobno, używając przycisku odtwarzania w marginesie edytora. Oczekuj kolejno wyników 64, 76, 0 i 10.
  2. Uruchom klasę RowLayoutTests przyciskiem odtwarzania obok jej deklaracji. Wszystkie cztery metody powinny przejść razem.
  3. Wybierz Product > Test, aby uruchomić aktywny plan testów. Upewnij się, że testy rzeczywiście zostały wykonane, a nie pominięte lub wykluczone.
  4. Jeśli dodano przykład Swift Testing, uruchom osobno i razem również jego dwie funkcje.

Jeśli pojedyncze testy przechodzą, ale cały plan zawodzi, prześledź obiekt używany przez zawodnego odbiorcę. Sprawdź, czy pominięto wstrzyknięcie, czy funkcja pomocnicza wywołuje .shared albo czy praca asynchroniczna trwa po zakończeniu testu, który ją rozpoczął. Jeśli test zawodzi samodzielnie, sprawdź jego dane wejściowe i oczekiwane obliczenie, zanim przypiszesz błąd kolejności w zestawie.

Gotowy fixture powinien jawnie pokazywać stan początkowy w ścieżce tworzenia bieżącego testu. W tych obliczeniach układu każdy test ma własny kalkulator, jawnie przekazuje go odbiorcy i oczekuje na jego działanie. Dzięki temu oczekiwany wynik nie zależy od kolejności wcześniejszych testów.

Źródła