Umieść wspólne, asynchroniczne przygotowanie fixture w init() async throws zestawu testów Swift Testing. Swift Testing kończy działanie tego inicjalizatora przed wywołaniem metody testowej instancji, więc metoda otrzymuje gotowy do użycia stan. W XCTest użyj zamiast tego setUp() async throws.
Ten poradnik jest przeznaczony dla dewelopera aplikacji Mac, który przygotowuje lokalne dane do testów jednostkowych w Xcode. Celem jest zakończenie asynchronicznej inicjalizacji przed rozpoczęciem sprawdzeń, przy użyciu deterministycznych obliczeń na syntetycznych danych wejściowych. Apple opisuje podejście z inicjalizatorem w Migrating a test from XCTest.
Jak Swift Testing czeka na inicjalizację fixture?
W przypadku metody testowej instancji Swift Testing inicjalizuje typ zawierający zestaw testów, a następnie wywołuje metodę na tej instancji. Zestaw musi mieć inicjalizator, który można wywołać bez argumentów; inicjalizator może być asynchroniczny, zgłaszać błędy lub spełniać oba te warunki. Każda metoda testowa instancji działa na osobnej instancji. Te reguły cyklu życia opisano w Organizing test functions with suite types.
Wynikająca z tego zależność jest prosta: przygotowanie fixture zwraca wynik, inicjalizator kończy działanie, a metoda testowa się rozpoczyna. Inicjalizator musi faktycznie oczekiwać na operację przygotowania. Samo rozpoczęcie pracy w jego wnętrzu nie ustanawia tej zależności.
Skorzystaj z poniższej tabeli, aby zdecydować, gdzie umieścić przygotowanie:
| Sytuacja | Miejsce przygotowania | Co zapewnia gotowość? |
|---|---|---|
| Kilka testów instancji korzysta z tej samej procedury przygotowania fixture | init() async throws zestawu |
Inicjalizator z await kończy działanie przed każdą metodą |
| Fixture jest potrzebny tylko w jednym teście | Treść testu async throws |
Jawne try await przed sprawdzeniami |
Istniejący test dziedziczy po XCTestCase |
setUp() async throws |
XCTest kończy asynchroniczne przygotowanie przed metodą |
Test Swift Testing jest static lub globalny |
Treść testu albo jawnie wywoływana funkcja pomocnicza | Test jawnie czeka na przygotowanie |
Ta tabela stosuje udokumentowane reguły cyklu życia obu frameworków do wyboru miejsca przygotowania fixture. W szczególności inicjalizator zestawu to punkt przygotowania dla każdej instancji; nie oznacza to, że przygotowanie odbywa się raz dla całego zestawu. Zobacz suite initialization rules i XCTest setup sequence firmy Apple.
Dodaj kompletny przykład asynchronicznego fixture w Xcode
Użyj Xcode 16 lub nowszego z dołączoną obsługą Swift Testing. Dokumentacja projektu Swift informuje, że Swift Testing jest dołączony do Swift 6 i Xcode 16, więc dodatkowa zależność pakietu nie jest potrzebna. Zobacz oficjalny Swift Testing README.
W istniejącym celu testów jednostkowych wybierz File > New > New File From Template, a następnie Swift Testing Unit Test. Zastąp wygenerowaną zawartość pliku kompletnym przykładem poniżej. Istniejący pakiet testów jednostkowych XCTest może również zawierać testy Swift Testing; osobny pakiet nie jest potrzebny. Te kroki są zgodne z instrukcją Apple Adding tests to your Xcode project.
import Testing
enum FixtureError: Error {
case invalidInput
}
private actor SquareCalculator {
let values: [Int]
init(values: [Int]) {
self.values = values
}
func squares() -> [Int] {
values.map { $0 * $0 }
}
}
struct CalculationFixture {
let squares: [Int]
let total: Int
static func make(values: [Int]) async throws -> Self {
guard !values.isEmpty,
values.count <= 100,
values.allSatisfy({ (0...100).contains($0) }) else {
throw FixtureError.invalidInput
}
let calculator = SquareCalculator(values: values)
let squares = await calculator.squares()
return Self(
squares: squares,
total: squares.reduce(0, +)
)
}
}
@Suite
struct CalculationTests {
let fixture: CalculationFixture
init() async throws {
fixture = try await CalculationFixture.make(values: [2, 4, 6])
}
@Test
func squaresAreReady() {
#expect(fixture.squares == [4, 16, 36])
}
@Test
func totalIsReady() {
#expect(fixture.total == 56)
}
}
Przykład używa prawdziwego lokalnego aktora do wykonania obliczeń. Wywołanie jego izolowanej metody spoza aktora wymaga await. Nie ma tu opóźnienia, losowych danych wejściowych, zewnętrznej zależności ani operacji w tle, która musi zakończyć się w odpowiednim momencie. Wynik pozostaje deterministyczny niezależnie od tego, czy wykonanie rzeczywiście zostanie wstrzymane.
Fabryka fixture odrzuca puste dane wejściowe, więcej niż 100 wartości oraz wartości spoza zakresu 0...100. Te przykładowe ograniczenia upraszczają obliczenia: każdy kwadrat ma wartość najwyżej 10 000, a suma najwyżej 1 000 000. Nieprawidłowe wartości powodują błąd zamiast cichego usunięcia lub zastąpienia.
Dla podanych danych wejściowych oczekiwany wynik obliczeń to 2² + 4² + 6² = 4 + 16 + 36 = 56. Inicjalizator zapisuje gotowy fixture we właściwości let, która nie jest opcjonalna. Metody testowe nie potrzebują więc ani flagi gotowości, ani rozpakowywania wartości opcjonalnej.
Same metody mogą pozostać synchroniczne, ponieważ ich asynchroniczny warunek wstępny został już spełniony w inicjalizatorze. Gdy praca asynchroniczna należy do treści testu, udokumentowany przez Apple wzorzec polega na oznaczeniu tej funkcji jako async i bezpośrednim oczekiwaniu na operację. Zobacz Testing asynchronous code.
Poprawność syntetycznych obliczeń sprawdzono lokalnie w Pythonie. W tym środowisku badawczym Swift jest niedostępny, więc przykłady Swift zostały przejrzane, ale nie zostały tutaj skompilowane ani uruchomione. Powyższe oczekiwane wyniki opisują obliczenia z przykładu, a nie rezultat uruchomienia testów w Xcode.
Dlaczego samo uruchomienie zadania nie kończy przygotowania?
Częstym błędem jest uruchomienie Task { ... } z synchronicznego inicjalizatora i natychmiastowy powrót. Inicjalizator kończy wtedy uruchamianie pracy, ale może ona nadal trwać w chwili rozpoczęcia metody testowej.
Przewodnik po języku Swift opisuje Task { ... } jako tworzenie zadania nieustrukturyzowanego. Zwróconego uchwytu można użyć do oczekiwania na wynik przez await handle.value. Odrzucenie uchwytu usuwa ten jawny punkt oczekiwania. Zobacz sekcję Unstructured Concurrency w oficjalnej książce o Swift.
W przypadku inicjalizacji fixture najczytelniej jest zwykle bezpośrednio oczekiwać na fabrykę w asynchronicznym inicjalizatorze. Jeśli istniejąca funkcja pomocnicza uruchamia pracę wewnętrznie i zwraca wynik przed jej zakończeniem, oczekiwanie na funkcję czeka tylko na jej powrót. Zmień kontrakt zakończenia tak, aby powrót oznaczał gotowość fixture, albo udostępnij wynik zadania, na który wywołujący może zaczekać.
Dodanie stałego opóźnienia nie wyraża tej zależności. Czeka jedynie na upływ czasu, przez co gotowość zależy od tego, czy przygotowanie zakończy się w wyznaczonym przedziale. Podobnie oznaczenie metody testowej jako async jedynie umożliwia wywołania asynchroniczne; metoda nadal musi czekać na operację, od której zależy.
Jaki jest odpowiednik przygotowania w XCTest?
XCTest udostępnia dedykowany punkt asynchronicznego przygotowania. Udokumentowana kolejność przed każdą metodą testową to setUp() async throws, następnie setUpWithError(), a potem synchroniczne setUp(). Po tych punktach wywoływana jest metoda testowa. Zobacz Set Up and Tear Down State in Your Tests.
Aby porównać ten sam fixture w XCTest, dodaj poniższy kompletny plik towarzyszący do tego samego celu testów jednostkowych co pierwszy przykład. Importuje XCTest i używa zdefiniowanego wcześniej typu CalculationFixture, widocznego w module. Definicje funkcji pomocniczych pozostaw w pierwszym pliku; nie duplikuj ich.
import XCTest
final class CalculationXCTests: XCTestCase {
private var fixture: CalculationFixture?
override func setUp() async throws {
try await super.setUp()
fixture = try await CalculationFixture.make(values: [2, 4, 6])
}
func testTotalIsReady() throws {
let readyFixture = try XCTUnwrap(fixture)
XCTAssertEqual(readyFixture.squares, [4, 16, 36])
XCTAssertEqual(readyFixture.total, 56)
}
}
Metoda XCTest również może być synchroniczna: asynchroniczne przygotowanie odbywa się podczas konfiguracji. Właściwość opcjonalna uwzględnia osobną fazę konfiguracji XCTest, a XCTUnwrap jawnie zgłasza błąd, gdy brakuje fixture. Apple opisuje ten wzorzec rozpakowywania w migration guide.
Podczas konwersji tego testu na Swift Testing przenieś przygotowanie do inicjalizatora zestawu i użyj import Testing, @Test oraz mechanizmów sprawdzających Swift Testing. Metoda nazwana po prostu setUp w zwykłym zestawie Swift Testing nie jest udokumentowanym mechanizmem przygotowania; w ścieżce migracji Apple rodzinę metod przygotowujących zastępuje inicjalizacja.
Sprawdź te kwestie, jeśli inicjalizacja wciąż wydaje się zbyt wczesna
Najpierw sprawdź, czy test jest metodą instancji. Test static nie wymaga instancji zestawu, więc nie może zależeć od automatycznej inicjalizacji instancji. Test globalny również nie ma instancji zawierającej. W obu przypadkach utwórz fixture i zaczekaj na niego w treści testu. Obsługiwane formy testów opisano w Migrating a test from XCTest.
Następnie przeanalizuj cały łańcuch wywołań przygotowania. Znajdź operację, która faktycznie tworzy gotowy stan, i upewnij się, że każdy wywołujący na nią czeka. W przykładzie obliczenia aktora kończą się przed zwróceniem wyniku przez fabrykę, a fabryka zwraca wynik przed zakończeniem inicjalizatora. Żadna praca potrzebna do wykonania sprawdzeń nie wymyka się z tej sekwencji.
Pozwól błędom konfiguracji propagować się dalej. Jeśli CalculationFixture.make zgłosi błąd, instancja zestawu nie może zakończyć inicjalizacji, więc jej metoda testowa nie może zostać na niej wykonana. Unikaj zastępowania nieudanego przygotowania pustym fixture za pomocą try? i wartości zapasowej, chyba że właśnie takie zachowanie jest przedmiotem testu. Przykładowy błąd nieprawidłowych danych celowo uniemożliwia utworzenie pozornie poprawnego fixture.
Na koniec odróżnij kolejność inicjalizacji od planowania testów. Domyślnie Swift Testing uruchamia testy równolegle. Modyfikator .serialized zestawu może wymusić sekwencyjne uruchamianie testów wchodzących w jego skład, ale serializacja nie zmienia nieoczekiwanego zadania w oczekiwanie na zakończenie przygotowania ani nie sprawia, że inicjalizator zestawu uruchamia się tylko raz. Apple opisuje zakres działania tego modyfikatora w Running tests serially or in parallel.
W przypadku tych lokalnych obliczeń pozostaw fixture niezmienny i przygotowuj go niezależnie dla każdej instancji testowej. Kluczowa jest zależność: operacja przygotowania zwraca gotowy fixture, inicjalizator czeka na ten wynik, a test odczytuje zainicjalizowaną właściwość. Dzięki temu test ma konkretny warunek gotowości i nie opiera się na założeniach dotyczących czasu.
Źródła
- Organizing test functions with suite types — Apple
- Migrating a test from XCTest — Apple
- Set Up and Tear Down State in Your Tests — Apple
- Swift Testing README — projekt Swift
- Adding tests to your Xcode project — Apple
- Testing asynchronous code — Apple
- The Swift Programming Language: Concurrency — projekt Swift
- Running tests serially or in parallel — Apple