Podczas migracji testów jednostkowych XCTest na Macu zachowaj kolejność związaną z własnością: zakończ pracę, zwolnij zamierzone silne referencje, a następnie sprawdź słabą referencję. W przypadku synchronicznego sprzątania lokalny dla testu blok defer jasno określa tę kolejność.

Ten przewodnik omawia konwersję testu jednostkowego w Xcode, w którym istotna jest własność obiektów. Przykład oblicza liczbę komórek w syntetycznej siatce, a następnie sprawdza, czy kalkulator został zwolniony. Wykorzystuje wyłącznie lokalne obiekty Swift i działania na liczbach całkowitych.

Jakie zachowanie cyklu życia musi zachować migracja?

XCTest zapewnia sekwencję zarządzaną przez framework. Po zakończeniu metody testowej uruchamia zarejestrowane bloki teardown w odwrotnej kolejności rejestracji, a następnie metody teardown instancji. Zarejestrowane sprzątanie nadal jest wykonywane, gdy asercja testu nie przejdzie; może mu zapobiec zakończenie procesu. Te szczegóły mają znaczenie, gdy jeden blok zwalnia obiekt, a inny sprawdza jego słabą referencję. Konfigurowanie i usuwanie stanu w testach

Na przykład blok teardown sprawdzający, czy wartość wynosi nil, musi wykonać się po bloku usuwającym ostatniego zamierzonego właściciela. Ponieważ XCTest używa odwrotnej kolejności rejestracji, najpierw rejestruje się sprawdzenie, a potem zwolnienie. Odwrócenie kolejności rejestracji zmienia to, co zaobserwuje test.

Przewodnik migracji Swift Testing mapuje konfigurację na inicjalizator, a teardown na deinicjalizator w klasie lub zestawie testów aktora. To mapowanie jest przydatne w przypadku sprzątania powiązanego z czasem życia zestawu. Asercja dotycząca własności wymaga dodatkowej decyzji: dokładnie która referencja ma zniknąć przed wykonaniem asercji? Migracja testu z XCTest

Przed zmianą składni testu skorzystaj z poniższej tabeli decyzyjnej:

Dotychczasowa odpowiedzialność Wybór przy migracji Uzasadnienie
Tworzenie świeżego stanu fixture dla każdej metody testowej instancji Inicjalizacja właściwości przechowywanej lub init() Ustanawia fixture przed uruchomieniem testu
Zwolnienie obiektu i natychmiastowe sprawdzenie słabej referencji Jawne zwolnienie, a następnie #expect, zwykle wewnątrz lokalnego defer Uwidacznia moment obserwacji
Synchroniczne sprzątanie związane z czasem życia zestawu opartego na typie referencyjnym Zestaw testów klasy lub aktora z deinit Jest zgodne z udokumentowanym mapowaniem cyklu życia
Oczekiwanie na sprzątanie lub zgłoszenie błędu sprzątania Oczekiwane sprzątanie w funkcji asynchronicznej albo odpowiedni niestandardowy zakres Wymaga ścieżki wykonania obsługującej zawieszenie i obsługę błędów

Tabela jest pomocą przy podejmowaniu decyzji migracyjnych, opracowaną na podstawie udokumentowanego cyklu życia i reguł języka. Nie oznacza to, że każdemu hookowi XCTest odpowiada zamienna metoda Swift Testing.

Dlaczego przeniesienie sprawdzenia słabej referencji do deinit może zmienić test?

Swift wywołuje deinicjalizator klasy bezpośrednio przed zwolnieniem jej instancji. Deinicjalizator nadal może uzyskiwać dostęp do przechowywanych właściwości tej instancji. Dlatego samo wejście do deinit zestawu nie dowodzi, że obiekt przechowywany w jednej z tych właściwości został już zwolniony. Deinicjalizacja

Rozważ zestaw testów z silną właściwością layout. Słaba referencja do tego układu może nadal mieć wartość inną niż nil podczas wykonywania deinicjalizatora zestawu. Test musi usunąć silną referencję właściwości przed sprawdzeniem, czy układ został zwolniony.

Istnieje też inny możliwy błąd: cykl referencji utrzymujący zestaw przy życiu może uniemożliwić wywołanie jego deinicjalizatora. Asercja umieszczona wyłącznie w tym deinicjalizatorze nie będzie wtedy mieć okazji zgłosić, że obiekt jest nadal utrzymywany. Wynika to z reguł własności ARC: silne referencje utrzymują instancje przy życiu, natomiast słabe referencje nie zapobiegają zwolnieniu i przyjmują wartość nil, gdy wskazywana instancja zostaje zwolniona. Automatyczne zliczanie referencji

W teście własności obiektów umieść obserwator poza obiektem, którego zwolnienie jest sprawdzane. Udostępnij testowi konkretną operację zwalniania i wykonaj jego asercję w zwykłym toku testu. Dzięki temu kontrakt testu można sprawdzić bez uzależniania go od dealokacji samego zestawu.

Ustal test bazowy XCTest w Xcode

Użyj istniejącego celu testów jednostkowych w Xcode 16 lub nowszym. Apple dokumentuje, że XCTest i Swift Testing mogą współistnieć w tym samym celu i pakiecie, więc migracja nie wymaga osobnego pakietu testów jednostkowych. Aby dodać plik testu, wybierz File > New > New File From Template i odpowiednio wybierz XCTest Unit Test albo Swift Testing Unit Test. Dodawanie testów do projektu Xcode

Poniżej znajduje się kompletny plik testu XCTest. Kalkulator zwraca 3 * 4, więc oczekiwany wynik to 12. Dwie rejestracje teardown celowo pokazują wymaganą kolejność wykonania.

import XCTest

private final class GridCalculator {
    func cellCount(columns: Int, rows: Int) -> Int {
        columns * rows
    }
}

final class GridCalculatorXCTests: XCTestCase {
    private var calculator: GridCalculator?

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

    func testCellCountAndRelease() {
        weak var observed = calculator

        // Registered first, so this executes second.
        addTeardownBlock { [weak observed] in
            XCTAssertNil(observed)
        }

        // Registered second, so this executes first.
        addTeardownBlock { [self] in
            calculator = nil
        }

        XCTAssertEqual(
            calculator?.cellCount(columns: 3, rows: 4),
            12
        )
    }
}

Blok zwalniania przechowuje referencję do przypadku testowego, aby móc zaktualizować właściwość fixture. Blok obserwacji przechwytuje kalkulator słabo, dzięki czemu samo sprawdzenie nie dodaje kolejnego silnego właściciela. Apple dokumentuje bloki teardown jako miejsce na sprzątanie specyficzne dla testu, rejestrowane wraz z powiązaną konfiguracją. Konfigurowanie i usuwanie stanu w testach

Kontrakt testu bazowego jest ściśle określony: po zakończeniu treści testu i wyczyszczeniu właściwości fixture kalkulator nie może już istnieć. Test nie mierzy zużycia pamięci ani nie potwierdza zwolnienia każdego obiektu utworzonego przez aplikację.

Przenieś sekwencję własności do lokalnego zakresu

W tym synchronicznym przypadku użyj zestawu typu struct i umieść zwalnianie oraz weryfikację w jednym bloku defer. Swift wykonuje odroczony kod przy opuszczaniu jego zakresu, także w przypadku wczesnego zwrotu lub zgłoszenia błędu. Wiele bloków odroczonych jest wykonywanych w odwrotnej kolejności; pojedynczy blok ułatwia odczytanie sekwencji zwolnienia przed sprawdzeniem w tym przykładzie. Przepływ sterowania

Poniżej znajduje się kompletny, niezależny plik Swift Testing. Zastąp nim plik bazowy albo umieść przykłady w osobnych plikach, tak aby ich prywatne typy pomocnicze pozostały rozdzielone. Drugi test jest kontrolą diagnostyczną: pokazuje, że dodatkowy silny właściciel utrzymuje słabą referencję, dopóki ten właściciel nie zostanie usunięty.

import Testing

private final class GridCalculator {
    func cellCount(columns: Int, rows: Int) -> Int {
        columns * rows
    }
}

private final class GridFixture {
    var calculator: GridCalculator? = GridCalculator()
    var additionalOwner: GridCalculator?
}

struct GridCalculatorTests {
    @Test
    func cellCountAndRelease() throws {
        let fixture = GridFixture()
        weak var observed = fixture.calculator

        defer {
            fixture.calculator = nil
            #expect(observed == nil)
        }

        let result = try #require(
            fixture.calculator?.cellCount(columns: 3, rows: 4)
        )
        #expect(result == 12)
    }

    @Test
    func weakProbeDetectsAnAdditionalOwner() {
        let fixture = GridFixture()
        weak var observed = fixture.calculator

        fixture.additionalOwner = fixture.calculator
        fixture.calculator = nil
        #expect(observed != nil)

        fixture.additionalOwner = nil
        #expect(observed == nil)
    }
}

W pierwszym teście fixture jest właścicielem kalkulatora, a observed obserwuje go bez przejmowania własności. Obliczenie zwraca opcjonalną liczbę całkowitą, którą #require rozpakowuje. Żadna lokalna zmienna nie przechowuje rozpakowanego kalkulatora. Przy opuszczaniu zakresu odroczony blok czyści referencję fixture i sprawdza obserwatora.

Nieudane #expect zapisuje problem i pozwala na dalsze wykonywanie. Nieudane #require zgłasza błąd, zatrzymując tę ścieżkę wykonania. Ponieważ defer jest zarejestrowany przed wymaganiem, jego sprzątanie i sprawdzenie słabej referencji nadal się wykonują, gdy wymaganie zgłosi błąd. Oczekiwania i potwierdzenia, Obsługa błędów

Kontrola diagnostyczna pokazuje dwa odrębne oczekiwane stany: wyczyszczenie jednej z dwóch referencji właścicieli pozostawia obserwatora z wartością inną niż nil; wyczyszczenie pozostałej referencji sprawia, że przyjmuje on wartość nil. Są to przykładowe sprawdzenia własności, a nie wyniki pomiarów aplikacji. Przykłady Swift zostały sprawdzone, ale nie można ich było skompilować w środowisku badawczym, ponieważ nie było w nim dostępnego pliku wykonywalnego Swift.

Sprawdź granice funkcji pomocniczych, współbieżność i asynchroniczne sprzątanie

Lokalny defer należy do otaczającego go zakresu. Przeniesienie bloku odroczonego do funkcji pomocniczej tworzącej obiekt sprawia, że wykona się on po powrocie tej funkcji, czyli być może przed użyciem zwróconego obiektu w teście. Pozostaw blok w teście albo zaprojektuj funkcję pomocniczą, która wykonuje całą operację wewnątrz swojego zakresu. Otaczający zakres jest częścią kontraktu własności. Przepływ sterowania

Sprawdź też, gdzie znajduje się fixture. Swift Testing tworzy osobną instancję zestawu dla każdej metody testowej instancji, a testy w zestawie domyślnie uruchamia równolegle. Stan lokalny dla instancji lub testu pasuje do tego modelu; globalna fixture lub statyczna kolekcja wprowadza własność współdzieloną między testami. Organizowanie funkcji testowych za pomocą typów zestawów

Jeśli kod aplikacji na Maca wymaga głównego aktora, jawnie izoluj test Swift Testing za pomocą @MainActor. Przewodnik migracji zaznacza, że Swift Testing nie dziedziczy domyślnego zachowania głównego aktora XCTest dla synchronicznych metod testowych. Syntetyczny kalkulator powyżej nie ma takiego wymagania. Migracja testu z XCTest

Synchroniczny blok odroczony nie zastępuje oczekiwanego teardown. Odroczone akcje Swift nie mogą zgłosić błędu poza blokiem. Jeśli sprzątanie może się nie powieść, obsłuż i zgłoś ten błąd jawnie zamiast go ignorować. Umieść oczekiwane sprzątanie w asynchronicznej ścieżce wykonania i zadbaj o to, by docierały do niego zarówno ścieżka zakończona powodzeniem, jak i ścieżka zgłaszająca błąd. Obsługa błędów

Do wielokrotnego użytku, zarządzanej przez framework konfiguracji i procedury teardown, obecna wersja Swift Testing udostępnia TestScoping, który zapewnia niestandardowe wykonanie wokół testu lub zestawu. Apple podaje dostępność od Swift 6.1 i Xcode 16.3. To obsługiwany kierunek, gdy kilka testów potrzebuje wspólnego zakresu, ale wymaga niestandardowego traitu i dostawcy zakresu, a nie tylko zmiany nazwy addTeardownBlock. TestScoping

Diagnozowanie zmiany wyniku sprawdzenia słabej referencji po migracji

Jeśli zmigrowany test własności nie przechodzi, sprawdź po kolei następujące kwestie:

  1. Potwierdź moment obserwacji. Zamierzone zwolnienie musi nastąpić przed asercją słabej referencji.
  2. Sprawdź właściwości fixture. Pozostała właściwość może nadal przechowywać kalkulator, nawet po wyczyszczeniu innej właściwości.
  3. Sprawdź aliasy lokalne i przechwycenia w domknięciach. Rozpakowany obiekt lub silnie przechwycona referencja mogą przedłużyć własność do czasu wykonania sprawdzenia.
  4. Sprawdź zakres funkcji pomocniczej. Odroczone sprzątanie funkcji pomocniczej może nastąpić zbyt wcześnie względem zamierzonego kontraktu testu.
  5. Sprawdź niedokończone zadania. Jeśli rzeczywisty obiekt uruchamia pracę asynchroniczną, określ i zaczekaj na jej zakończenie przed zastosowaniem synchronicznego oczekiwania na zwolnienie.

Słaba referencja o wartości innej niż nil wskazuje obiekt, który nadal istnieje w momencie obserwacji. Zbadaj, kto jest jego pozostałym właścicielem, zanim zmienisz asercję lub dodasz opóźnienie. W tym syntetycznym przykładzie nie ma pracy asynchronicznej, na którą trzeba czekać, a kontrola diagnostyczna pokazuje dokładnie, jak dodatkowy właściciel wpływa na wynik.

Migracja jest zakończona, gdy obliczenie i asercje własności wyrażają ten sam zamierzony rezultat co test bazowy: obliczyć 12, usunąć zamierzoną własność fixture i sprawdzić zwolnienie za pomocą niezależnie przechowywanej słabej referencji. Zachowaj tę sekwencję w widocznym miejscu w miarę rozwoju testu.

Źródła