När du migrerar XCTest-enhetstester på en Mac ska du bevara ägarsekvensen: slutför arbetet, släpp de avsedda starka referenserna och kontrollera sedan den svaga referensen. För synkron upprensning gör en testspecifik defer den sekvensen tydlig.
Den här genomgången fokuserar på att konvertera ett ägarskapskänsligt enhetstest i Xcode. Exemplet beräknar antalet celler i ett syntetiskt rutnät och kontrollerar sedan att dess kalkylator har frigjorts. Det använder endast lokala Swift-objekt och heltalsaritmetik.
Vilket beteende i livscykeln måste migreringen bevara?
XCTest tillhandahåller en sekvens som hanteras av ramverket. När en testmetod avslutas körs registrerade teardown-block i omvänd registreringsordning, följt av instansens teardown-metoder. Registrerad upprensning körs fortfarande om ett testpåstående misslyckas; processterminering kan förhindra det. Dessa detaljer spelar roll när ett block släpper ett objekt och ett annat kontrollerar dess svaga referens. Konfigurera och ta bort tillstånd i dina tester
Ett teardown-block som kontrollerar nil måste till exempel köras efter blocket som tar bort den sista avsedda ägaren. Eftersom XCTest använder omvänd registreringsordning registreras kontrollen först och släppandet därefter. Om registreringarna görs i omvänd ordning förändras vad testet observerar.
Swift Testings migreringsguide kopplar setup till en initializer och teardown till en deinitializer i en klass- eller actorsvit. Den kopplingen är användbar för upprensning som hör till svitens livstid. Ett ägarskapspåstående kräver ett ytterligare beslut: exakt vilken referens ska försvinna innan påståendet körs? Migrera ett test från XCTest
Använd den här beslutstabellen innan du ändrar testets syntax:
| Befintligt ansvar | Migreringsval | Orsak |
|---|---|---|
| Skapa nytt fixture-tillstånd för varje instanstest | Initiering av lagrad egenskap eller init() |
Upprättar fixturen innan testet körs |
| Släpp ett objekt och kontrollera omedelbart en svag referens | Explicit släppning följd av #expect, vanligtvis i ett lokalt defer |
Gör observationspunkten synlig |
| Utför synkron upprensning som hör till livstiden för en referenstypssvit | Klass- eller actorsvit med deinit |
Följer den dokumenterade livscykelkopplingen |
| Vänta på upprensning eller rapportera ett upprensningsfel | Väntad upprensning i en asynkron funktion eller ett lämpligt anpassat scope | Kräver en körväg som stöder suspendering och felhantering |
Tabellen är ett stöd för migreringsbeslut som bygger på den dokumenterade livscykeln och språkreglerna. Den innebär inte att varje XCTest-hook har en utbytbar metod i Swift Testing.
Varför kan testet förändras om den svaga kontrollen flyttas till deinit?
Swift anropar en klassdeinitializer omedelbart innan instansen avallokeras. Deinitializern kan fortfarande komma åt instansens lagrade egenskaper. Därför bevisar det inte i sig att ett objekt som hålls av någon av dessa egenskaper redan har frigjorts när svitens deinit körs. Deinitialisering
Tänk dig en svit med en stark layout-egenskap. En svag referens till den layouten kan fortfarande vara icke-nil medan svitens deinitializer körs. Testet måste ta bort den starka egenskapsreferensen innan det kontrollerar om layouten har frigjorts.
Det finns ytterligare ett felsätt: en referenscykel som håller kvar sviten kan förhindra att dess deinitializer körs. Ett påstående som bara ligger i den deinitializern får då ingen möjlighet att rapportera kvarhållningen. Detta följer av ARC:s ägarskapsregler: starka referenser håller instanser vid liv, medan svaga referenser inte förhindrar avallokering och blir nil när den refererade instansen avallokeras. Automatisk referensräkning
Låt observatören ligga utanför objektet vars frigörande testas i ett ägarskapstest. Ge testet en specifik frigöringsåtgärd och kör dess påstående genom testets normala kontrollflöde. Då går testets kontrakt att granska utan att vara beroende av att själva sviten avallokeras.
Upprätta XCTest-utgångsläget i Xcode
Använd ett befintligt enhetstestmål i Xcode 16 eller senare. Apple dokumenterar att XCTest och Swift Testing kan samexistera i samma mål och paket, så migreringen kräver inget extra enhetstestpaket. Om du vill lägga till en testfil använder du File > New > New File From Template och väljer XCTest Unit Test eller Swift Testing Unit Test efter behov. Lägga till tester i ditt Xcode-projekt
Följande är en komplett XCTest-testfil. Kalkylatorn returnerar 3 * 4, vilket ger det förväntade resultatet 12. De två teardown-registreringarna visar avsiktligt den nödvändiga körordningen.
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
)
}
}
Släppblocket behåller testfallet så att det kan uppdatera fixture-egenskapen. Observationsblocket fångar kalkylatorn svagt och undviker därmed ytterligare en stark ägare i själva kontrollen. Apple dokumenterar teardown-block som platsen för testspecifik upprensning som registreras tillsammans med tillhörande setup. Konfigurera och ta bort tillstånd i dina tester
Utgångslägets kontrakt är snävt definierat: när testkroppen har körts klart och fixture-egenskapen har tömts ska kalkylatorn inte längre finnas. Testet mäter inte minnesförbrukning och visar inte att varje objekt som skapats av programmet har frigjorts.
Migrera ägarsekvensen till ett lokalt scope
För det här synkrona fallet använder du en struct-svit och lägger släppning och verifiering i ett enda defer-block. Swift kör uppskjuten kod när kontrollen lämnar det omslutande scopet, även vid en tidig retur eller ett kastat fel. Flera uppskjutna block körs i omvänd ordning; ett enda block gör sekvensen släpp-före-kontroll i det här exemplet lättare att läsa. Kontrollflöde
Följande är en komplett, fristående Swift Testing-fil. Ersätt utgångsfilen med den, eller behåll exemplen i separata filer där deras privata hjälptyper förblir separata. Det andra testet är en diagnostisk kontroll: det visar att ytterligare en stark ägare håller den svaga referensen ifylld tills ägaren rensas bort.
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)
}
}
I det första testet äger fixturen kalkylatorn, och observed bevakar den utan att äga den. Beräkningen ger ett valfritt heltal som #require packar upp. Ingen uppackad kalkylator lagras i en annan lokal variabel. När scopet avslutas tömmer det uppskjutna blocket fixturens referens och kontrollerar observatören.
Ett misslyckat #expect registrerar ett fel och låter körningen fortsätta. Ett misslyckat #require kastar ett fel och stoppar den körvägen. Eftersom defer registreras före kravet körs dess upprensning och svaga kontroll ändå när kravet kastar ett fel. Förväntningar och bekräftelser, Felhantering
Den diagnostiska kontrollen fastställer två separata förväntade tillstånd: om en av två ägare rensas bort förblir observatören icke-nil; om den återstående ägaren rensas bort blir den nil. Det här är illustrativa ägarskapskontroller, inte uppmätta resultat från ett program. Swift-exemplen granskades men kunde inte kompileras i forskningsmiljön eftersom ingen Swift-körbar fil fanns tillgänglig.
Kontrollera hjälpargränser, samtidighet och asynkron upprensning
Ett lokalt defer hör till det omslutande scopet. Om du flyttar det uppskjutna blocket till en fabrikshjälpare körs det när hjälparen returnerar, vilket kan ske innan testet använder det returnerade objektet. Behåll blocket i testet eller utforma en hjälpare som utför hela operationen inom sitt scope. Det omslutande scopet är en del av ägarskapskontraktet. Kontrollflöde
Kontrollera också var fixturen finns. Swift Testing skapar en separat svitinstans för varje instanstestmetod, och tester inom en svit körs som standard parallellt. Instanslokalt eller testlokalt tillstånd passar den modellen; en global fixtur eller statisk samling innebär ägarskap som delas mellan tester. Organisera testfunktioner med svittyper
För Mac-programkod som kräver huvudaktören isolerar du Swift Testing-testet uttryckligen med @MainActor. Migreringsguiden påpekar att Swift Testing inte ärver XCTest:s standardbeteende med huvudaktör för synkrona testmetoder. Den syntetiska kalkylatorn ovan har inget sådant krav. Migrera ett test från XCTest
Det synkrona uppskjutna blocket ersätter inte väntad teardown. Swift kan inte kasta ett fel ut ur uppskjutna åtgärder. Om upprensningen kan misslyckas ska du hantera och rapportera felet uttryckligen i stället för att ignorera det. Behåll väntad upprensning i en asynkron körväg och säkerställ att både lyckade körningar och körningar där kroppen kastar ett fel når den. Felhantering
För återanvändbar ramverkshanterad setup och teardown tillhandahåller Swift Testing numera TestScoping, som erbjuder anpassad körning runt ett test eller en svit. Apple anger att det är tillgängligt från Swift 6.1 och Xcode 16.3. Det är en stödd väg när flera tester behöver ett gemensamt scope, men kräver en anpassad trait och scope-provider i stället för att bara döpa om addTeardownBlock. TestScoping
Diagnostisera en svag kontroll som förändras efter migreringen
Om det migrerade ägarskapstestet misslyckas undersöker du följande punkter i ordning:
- Bekräfta observationspunkten. Den avsedda frigöringen måste ske före det svaga påståendet.
- Granska fixture-egenskaperna. En kvarvarande egenskap kan fortfarande äga kalkylatorn efter att en annan egenskap har tömts.
- Granska lokala alias och closure-fångster. Ett uppackat objekt eller en starkt fångad referens kan förlänga ägarskapet fram till kontrollen.
- Granska hjälparens scope. En hjälpares uppskjutna upprensning kan köras för tidigt för testets avsedda kontrakt.
- Granska pågående arbete. Om det verkliga objektet startar asynkront arbete ska du definiera och invänta att det slutförs innan du tillämpar en synkron förväntning om frigöring.
En icke-nil svag referens visar att ett objekt fortfarande finns vid observationspunkten. Undersök den kvarvarande ägaren innan du ändrar påståendet eller lägger till en fördröjning. I det här syntetiska exemplet finns inget asynkront arbete att invänta, och den diagnostiska kontrollen visar exakt hur ytterligare en ägare påverkar resultatet.
Migreringen är klar när beräkningen och ägarskapspåståendena uttrycker samma avsedda resultat som utgångsläget: beräkna 12, ta bort fixturens avsedda ägarskap och verifiera frigöringen genom en separat hållen svag referens. Gör sekvensen tydlig när testet utvecklas.
Källor
Konfigurera och ta bort tillstånd i dina tester — Apple Developer Documentation.
Migrera ett test från XCTest — Swift-projektet.
Deinitialisering — Swift-projektet, The Swift Programming Language.
Automatisk referensräkning — Swift-projektet, The Swift Programming Language.
Lägga till tester i ditt Xcode-projekt — Apple Developer Documentation.
Kontrollflöde — Swift-projektet, The Swift Programming Language.
Förväntningar och bekräftelser — Swift-projektet.
Felhantering — Swift-projektet, The Swift Programming Language.
Organisera testfunktioner med svittyper — Swift-projektet.
TestScoping — Apple Developer Documentation.