För att få XCTest-tester att godkännas både var för sig och som en testsvit ska varje test ha en egen muterbar fixtur som injiceras i koden som testas. När en singleton måste förbli delad återställer du den före varje test och förhindrar överlappande åtkomst under hela testets livslängd.

Den här guiden följer en utvecklingsuppgift på Mac: att åtgärda layoutberäkningstester i Xcode som godkänns när de körs ensamma men misslyckas när de körs tillsammans. Exemplet använder endast syntetiska heltalsbredder och lokal aritmetik. Det kräver inget appfönster, tjänstemock, fillagring eller nätverksanslutning.

Varför kan ett test godkännas ensamt men misslyckas i testsviten?

Ett test som körs isolerat kan möta en singletons ursprungliga tillstånd. I en testsvit kan ett tidigare test redan ha ändrat det. En ny referens till singletonen pekar fortfarande på samma muterbara objekt.

Swift skiljer mellan instansegenskaper och typegenskaper: varje instans har sina egna instansegenskaper, medan en typegenskap har en enda delad kopia. Lagrade typegenskaper initieras en gång vid första åtkomst. Därför skapar en tilldelning av SpacingCalculator.shared till en testegenskap inte en ny kalkylator. Se Swift-projektets dokumentation om egenskaper.

Tänk dig en syntetisk rad med objekt som har bredderna [10, 20, 30]. Den totala bredden är summan av objektens bredder plus mellanrummen mellan dem. Med ett standardmellanrum på 2 blir resultatet 64; med ett mellanrum på 8 blir det 76.

Anta att ett test kontrollerar standardbredden utan att konfigurera kalkylatorn, medan ett annat ändrar det delade mellanrummet till 8. Följande tabell beskriver sekventiell körning, där varje rad börjar med en ny kalkylator med mellanrummet 2. Det här är illustrativa beräkningar, inte registrerade testresultat från Xcode.

Körning Delad kalkylator utan återställning Ny kalkylator för varje test
Test med standardvärde, ensamt 64: godkänns 64: godkänns
Test med stort mellanrum, ensamt 76: godkänns 76: godkänns
Standardvärde, sedan stort mellanrum Båda godkänns Båda godkänns
Stort mellanrum, sedan standardvärde Standardtestet får 76: misslyckas Båda godkänns

Det här ger dig en användbar ledtråd: undersök vilket test som ändrar tillståndet som används i det misslyckade kontrollvillkoret. Om du ändrar testnamnen för att få en gynnsam körordning finns beroendet fortfarande kvar.

Vilken XCTest-livscykelmetod bör äga fixturen?

Använd en instansbaserad setupmetod för muterbart tillstånd som varje test behöver. Apple dokumenterar att instansbaserad setup körs före varje testmetod, medan klassbaserad setup körs en gång före testklassen. En muterbar fixtur som skapas i klassbaserad setup får därför inte ett nytt utgångsläge mellan metoderna. Se Konfigurera och rensa tillstånd i dina tester.

Välj den setupmetod som passar arbetet: synkron, icke-throwing konstruktion av fixturen passar setUp(); konstruktion som kan kasta fel passar setUpWithError(); asynkron förberedelse passar setUp() async throws. Använd teardown för att slutföra det aktuella testets rensning. Apple avråder uttryckligen från att förlita sig på teardown för att förbereda efterföljande tester, eftersom en krasch kan hindra teardown från att köras. Samma livscykeldokumentation beskriver dessa gränser.

Valet av fixtur beror både på ägarskap och körningsomfattning:

Situation Lämplig metod Kontrollera att
Testerna kan skapa beroendet Skapa och injicera en instans per test Konsumenten använder den injicerade instansen
Testerna måste använda den faktiska singletonen Återställ ett fullständigt utgångsläge före varje test Inga överlappande tester kan ändra den singletonen
Delad konfiguration är oföränderlig Återanvänd det oföränderliga värdet Även refererade objekt förblir oförändrade
Ett test startar asynkront arbete Vänta tills arbetet är klart innan testet avslutas Inget oavslutat arbete kan ändra fixturen efteråt

Det första alternativet är enklast för den här kalkylatorn. Produktionskoden kan behålla ett delat standardvärde samtidigt som testerna uttryckligen väljer oberoende instanser.

Skapa en XCTest-fixtur med eget ägarskap

Använd en Mac med Xcode och ett befintligt macOS-mål för enhetstester. Lägg till följande fullständiga exempel som en Swift-fil i det testmålet. Det definierar en egen kalkylator, konsument och tester, så det kräver ingen import av appmodulen. Använd en Swift 6-verktygskedja med målets standardisolering för aktörer inställd på nonisolated.

Kalkylatorn är en aktör, vilket tydliggör samtidighetsgränsen för dess muterbara tillstånd. Swift-aktörer begränsar åtkomsten till sitt muterbara tillstånd, och anrop utifrån använder await. Reglerna beskrivs i Swift-projektets kapitel om samtidighet. Oberoende ägarskap för fixturer behövs även med en aktör.

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

Den avgörande raden är calculator = SpacingCalculator(). Varje setup skapar en oberoende aktör med mellanrummet 2. Varje test skickar sedan aktören till RowLayout. Om du anropar RowLayout() i de här testerna väljs .shared i stället, och fixturen kringgås.

Exemplet tillåter medvetet direkt konstruktion. Om din befintliga singleton har en privat initialiserare kan du införa en intern väg för konstruktion eller bryta ut dess muterbara beräkningstillstånd till ett separat beroende som kan konstrueras. En intern initialiserare kan göras tillgänglig för ett testmål via appmodulens etablerade upplägg för testbar import; det behövs inte att singleton-referensen kan ersättas globalt.

Aritmetiken definierar också två användbara gränsfall: en tom rad har bredden 0, och ett enda objekt har inget mellanrum. Exemplet godtar icke-negativa heltal som är tillräckligt små för att undvika heltalsöverskridning; negativa värden bryter mot förvillkoren och avslutar körningen i stället för att ge ett misslyckat kontrollvillkor.

Den förväntade aritmetiken och båda ordningarna för två test kontrollerades lokalt med deterministiska Python-beräkningar. Swift-exemplen granskades men kompilerades eller kördes inte eftersom den här forskningsmiljön saknar en Swift-verktygskedja. Kör dem i Xcode för att verifiera kompilering och körning för ditt mål.

När räcker det att återställa singletonen?

Återställning är en fungerande övergångslösning när testerna faktiskt behöver den delade instansen och hela dess testsynliga tillstånd kan återställas. Återskapa utgångsläget före varje test, inklusive värden som testet förutsätter är standardvärden. Om du bara återställer egenskapen som ändrades av det test som för närvarande misslyckas kan andra beroenden förbli oupptäckta.

För den här kalkylatorn kan asynkron setup välja SpacingCalculator.shared och invänta setGap(2) före varje metod. Det återställer aritmetikens utgångsläge endast om ingen överlappande anropare ändrar mellanrummet före kontrollen. Om fixturen senare får cachade resultat eller ytterligare konfiguration måste även dessa värden återställas.

En aktör ger inte ett enskilt test ägarskap över flera väntade anrop. Till exempel kan test A ställa in det delade mellanrummet till 2, test B kan ställa in det till 8, och test A kan sedan beräkna en bredd med 8. Varje aktörsoperation kan köras korrekt samtidigt som testets övergripande antagande fallerar. Detta följer av Swifts dokumenterade regler för aktörskörning och avstängning.

Innan du väljer isolering baserad på återställning bör du identifiera alla tester och bakgrundsuppgifter som kan komma åt det delade objektet. Om du inte kan säkerställa exklusiv användning under förberedelse, körning och rensning bör du i stället injicera oberoende tillstånd. Att återställa en ögonblicksbild efteråt hindrar inte en annan anropare från att observera tillfälliga ändringar under testet.

Hur fungerar fixturen med Swift Testing?

Swift Testing initierar en separat svitinstans för varje instanstestfunktion. En instansegenskap som skapar en kalkylator ger därför samma ägarskapsmönster utan att XCTest-setuppmetoder behöver åsidosättas. En egenskap som initieras från .shared skulle fortfarande referera till singletonen. Apple beskriver instanslivscykeln i Organisera testfunktioner med svittyper.

När de tidigare definitionerna av SpacingCalculator och RowLayout finns en gång i samma testmål är detta en komplett, ytterligare Swift Testing-svit. Den kräver en verktygskedja som innehåller modulen Testing, till exempel Xcode 16 eller senare. Dess standardisolering för aktörer bör också vara 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)
    }
}

För mer omfattande förberedelser stöder Swift Testing svitinitialiserare, inklusive asynkrona initialiserare och sådana som kan kasta fel. Apples Migrera ett test från XCTest beskriver denna ersättning för XCTest-setuppmetoder. Den här fixturen behöver ingen uttrycklig rensning eftersom den bara äger lokal beräkningstillstånd och inte startar något bakgrundsarbete.

Swift Testing kör som standard tester parallellt. Om du tillämpar .serialized på en svit körs dess tester och nästlade sviter sekventiellt, men det hindrar inte orelaterade tester från att köras samtidigt. Om attributet tillämpas på en vanlig, icke-parametriserad testfunktion har det ingen effekt. Dessa begränsningar anges uttryckligen i Kör tester sekventiellt eller parallellt.

Därför ger två singleton-beroende sviter med separata .serialized-annoteringar ingen ömsesidig uteslutning mellan dem. En gemensam serialiserad överordnad svit kan samordna sina underordnade sviter, förutsatt att alla konkurrerande användare ingår i dess omfattning. Med nya fixturer slipper de här beräkningstesterna den samordningen.

Verifiera resultaten för enskilda tester och testsviten i Xcode

Använd samma schema, testplan och Mac-destination genom hela verifieringen. Apple beskriver hur du kör enskilda metoder, sviter och den aktiva testplanen i Köra tester och tolka resultat.

  1. Kör var och en av de fyra XCTest-metoderna individuellt med uppspelningsknappen i redigerarens marginal. Förvänta dig 64, 76, 0 respektive 10.
  2. Kör klassen RowLayoutTests med uppspelningsknappen bredvid dess deklaration. Alla fyra metoder bör godkännas tillsammans.
  3. Välj Product > Test för att köra den aktiva testplanen. Kontrollera att dessa tester faktiskt kördes och inte hoppades över eller uteslöts.
  4. Om du lade till Swift Testing-exemplet kör du även dess två funktioner var för sig och tillsammans.

Om enskilda tester godkänns men hela testplanen misslyckas bör du spåra vilket objekt den felande konsumenten använder. Leta efter en missad injicering, en hjälpfunktion som anropar .shared eller asynkront arbete som fortsätter efter att det test som startade det har avslutats. Om ett test misslyckas ensamt bör du kontrollera dess indata och förväntade beräkning innan du tillskriver felet testsvitens körordning.

Den färdiga fixturen bör göra starttillståndet synligt i det aktuella testets konstruktionsväg. För de här layoutberäkningarna äger varje test en kalkylator, skickar den uttryckligen till konsumenten och inväntar dess arbete. Då är det förväntade resultatet oberoende av vilket närliggande test som kördes först.

Källor