Combine-Abbruch und Objektlebensdauer auf einem Cloud-Mac testen
Wenn Suchergebnisse nach dem Verlassen der Ansicht weiterhin in den Zustand geschrieben werden oder bei jedem erneuten Öffnen mehr Anfragen entstehen, liegt das häufig nicht an einem langsamen Netzwerk, sondern an zu langlebigen Combine-Subscriptions. Ersetzen Sie bei Regressionstests auf einem Cloud-Mac das echte Netzwerk zunächst durch eine kontrollierbare Ereignisquelle: Legen Sie fest, wann eine Subscription beginnt, wann sie abgebrochen wird und welches Objekt freigegeben werden soll. Binden Sie erst danach den tatsächlichen Dienst wieder an. So lassen sich Fehler zuverlässig reproduzieren, ohne auf Antworten eines entfernten Servers warten zu müssen.
Die Garantien beim Abbruch getrennt betrachten
Bei einem Abbruch sind mindestens drei Ergebnisse zu unterscheiden: Nachgelagerte Empfänger erhalten keine Werte mehr, die vorgelagerte Quelle erhält das Abbruchsignal, und die zugrunde liegende Aufgabe wird beendet. Die ersten beiden Punkte lassen sich mit Tests der Publisher-Pipeline prüfen. Für den dritten muss die konkrete Aufgabenimplementierung untersucht werden; er lässt sich nicht aus dem Zustand der Subscription ableiten.
Definieren Sie für die Ansicht vier Abnahmekriterien: Nach dem Abbruch bleibt der Wert unverändert; ein wiederholter Abbruch führt nur einmal zum Aufräumen; das Ansichtsmodell wird freigegeben; eine alte Anfrage kann das Ergebnis einer neuen nicht überschreiben. Das reguläre Abschließen braucht einen eigenen Testfall, denn eine Abschlussbenachrichtigung ist keine Abbruchbenachrichtigung.
cancel ist kein Rollback. Bereits geschriebener Zustand, auf dem Datenträger gespeicherte Dateien und bereits an den Server übergebene Operationen werden nicht automatisch rückgängig gemacht.
Legen Sie zunächst die verwendete Xcode-Toolchain fest und nehmen Sie die Tests in das vorhandene XCTest-Unit-Test-Target auf. Die folgenden Beispiele benötigen nur Combine und XCTest. Weder muss die Oberfläche im Simulator gestartet werden, noch sind Netzwerkberechtigungen erforderlich.
Die Weitergabe des Abbruchs mit einer synchronen Ereignisquelle prüfen
Fügen Sie den folgenden Code zum Test-Target hinzu. Der Zähler dient hier ausschließlich dem Test; verwenden Sie im Produktivcode keine globalen Zähler als Abbruchstatus. Der Test sendet zunächst einen Wert, bricht die Subscription zweimal ab und sendet anschließend einen weiteren Wert. Die drei Assertions prüfen nacheinander den Empfang, die Weitergabe des Abbruchs und das Ausbleiben weiterer Werte nach dem Abbruch.
import Combine
import XCTest
final class SubscriptionLifetimeTests: XCTestCase {
func testCancelStopsDeliveryAndPropagatesOnce() {
let source = PassthroughSubject<Int, Never>()
var received: [Int] = []
var cancellations = 0
let token = source
.handleEvents(receiveCancel: {
cancellations += 1
})
.sink { value in
received.append(value)
}
source.send(1)
XCTAssertEqual(received, [1])
token.cancel()
token.cancel()
source.send(2)
XCTAssertEqual(cancellations, 1)
XCTAssertEqual(received, [1])
}
}
Da diese Pipeline keinen Scheduler-Wechsel enthält, sendet PassthroughSubject synchron. Die Assertions können daher unmittelbar auf die jeweiligen Aktionen folgen. Fügen Sie kein sleep ein, um „dem System etwas Zeit zu geben“: Das würde nur eine unklare Ereignisreihenfolge verdecken.
Dieser Test prüft die Pipeline aus dem Beispiel. Wenn Sie Ihren eigenen Dienst anbinden, müssen Sie die Ereignisquelle per Dependency Injection an das Geschäftsmodell übergeben und dieselben Assertions anhand des öffentlich zugänglichen Modellzustands ausführen. Eine isolierte Pipeline ohne Bezug zum Produkt zu testen, reicht nicht aus.
Besitzverhältnisse prüfen, nicht nur cancel
Ein typischer Retain Cycle entsteht, wenn das Modell ein AnyCancellable hält und die sink-Closure wiederum das Modell stark referenziert. Soll die Subscription erst bei der Freigabe des Modells freigegeben werden, warten beide aufeinander. Das folgende Beispiel unterbricht den Zyklus durch einen schwachen Capture und prüft anhand einer schwachen Referenz, ob das Objekt tatsächlich freigegeben wurde.
private final class ScreenModel {
var value = 0
private var token: AnyCancellable?
init(source: PassthroughSubject<Int, Never>) {
token = source.sink { [weak self] value in
self?.value = value
}
}
}
extension SubscriptionLifetimeTests {
func testModelReleaseCancelsSubscription() {
let source = PassthroughSubject<Int, Never>()
var cancellations = 0
let publisher = source.handleEvents(receiveCancel: {
cancellations += 1
})
var token: AnyCancellable?
weak var weakModel: ScreenModel?
do {
let model = ScreenModel(source: source)
weakModel = model
token = publisher.sink { [weak model] value in
model?.value = value
}
}
XCTAssertNil(weakModel)
token?.cancel()
XCTAssertEqual(cancellations, 1)
}
}
Der zweite Testfall betrachtet die Freigabe des Modells und den Abbruch der externen Subscription getrennt, statt beides als einen Vorgang zu behandeln. Die interne Subscription wird mit dem Modell freigegeben und dabei abgebrochen. Das externe token wird dagegen weiterhin vom Test gehalten und muss ausdrücklich beendet werden. Ergänzen Sie bei der Übernahme in ein reales Projekt eine Beobachtungsstelle für den Abbruch der internen Subscription und prüfen Sie beide Subscriptions separat.
Ein schwacher Capture ist ebenfalls kein Allheilmittel. Muss eine Aufgabe auch nach dem Verlassen der Ansicht weiterlaufen, sollte ein langlebigerer Dienst sie halten. Die Ansicht abonniert dann lediglich den Fortschritt. So wird die Aufgabe nicht versehentlich beendet, sobald die Ansicht freigegeben wird.
Asynchrone Zustandsupdates durch eine Kennungsprüfung absichern
Wird Suche A abgebrochen und anschließend Suche B gestartet, können bereits in andere Ausführungswarteschlangen eingereihte Arbeiten weiterhin versuchen, Ergebnisse zurückzuschreiben. Ein bereits aufgerufener Abbruch ist keine Sicherheitsbarriere für den Zustand.
Vergeben Sie für jede Anfrage eine fortlaufende Kennung und prüfen Sie diese beim Zurückschreiben. Sowohl Änderungen an der Kennung als auch das Schreiben des Zustands müssen innerhalb desselben actor oder seriellen Ausführungskontexts erfolgen. Legen Sie die Testreihenfolge fest: A starten, B starten, die Antwort von B liefern und zuletzt die Antwort von A liefern. Der Endzustand muss weiterhin zu B gehören.
Ergänzen Sie einen Test für das Verlassen der Ansicht: Anfrage starten, Ansicht verlassen, eine verspätete Antwort auslösen und sicherstellen, dass der sichtbare Zustand unverändert bleibt. Darf die Aufgabe gemäß der Geschäftslogik im Hintergrund weiterlaufen, prüfen Sie getrennt, dass sich der Dienstzustand aktualisiert und die Callbacks an die Ansicht aufhören.
Wenn receive(on:) beteiligt ist, prüfen Sie zuerst den synchronen Kern und anschließend die Scheduler-Grenze. Lassen Sie die Warteschlange mit einem kontrollierbaren Scheduler gezielt weiterlaufen. Fehlt eine solche Infrastruktur, warten Sie auf ein eindeutiges Signal für den Abschluss der Ausführung. Eine bloße Wartezeit belegt nicht, dass keine verspäteten Ereignisse mehr eintreffen.
Regressionstests so gestalten, dass sich Fehler eingrenzen lassen
Protokollieren Sie bei jedem fehlgeschlagenen Testfall die Anfragekennung, die Ereignisreihenfolge, die Anzahl der Abbrüche und den Endzustand, aber keine Anfrage-Tokens oder echten Nutzerdaten. Ändern Sie jeweils nur eine Bedingung: Prüfen Sie zunächst eine einzelne Subscription und decken Sie danach schnelles erneutes Abonnieren, regulären Abschluss, Abschluss mit Fehler und die Freigabe der Ansicht ab.
Prüfen Sie beim Anbinden echter Netzwerkaufgaben separat, ob der Abbruch-Callback cancel an der zugrunde liegenden Aufgabe aufruft und welchen Endzustand die Aufgabe erreicht. Dass der Server kein Ergebnis geliefert hat, ist allein kein Erfolgsnachweis. Bei einem Rennen zwischen Abbruch und Abschluss müssen vertragskonforme Beendigungsreihenfolgen zulässig sein; nach der Beendigung darf jedoch kein Geschäftszustand mehr zurückgeschrieben werden.
Führen Sie zum Schluss eine Gegenprobe mit absichtlich eingebauten Fehlern durch: Ohne schwachen Capture muss der Retain Cycle sichtbar werden; ohne Prüfung der Anfragekennung muss der Test für Antworten in vertauschter Reihenfolge fehlschlagen. Erst wenn die Tests gezielt eingebrachte Fehler erkennen, zeigen sie, dass sie die tatsächlichen Grenzen absichern — und nicht nur die Pipeline grün halten.
Häufig gestellte Fragen
Stoppt cancel immer auch die Netzwerkaufgabe?
Nein. Das hängt von der Implementierung des Publishers ab. Prüfen Sie zusätzlich, ob dessen Abbruchpfad die zugrunde liegende Aufgabe tatsächlich abbricht.
Wie lassen sich feste Wartezeiten vermeiden?
Steuern Sie synchrone Ereignisse mit PassthroughSubject. Für asynchrone Zustellung verwenden Sie einen steuerbaren Scheduler oder ein eindeutiges Abschlusssignal.
Miete einen Cloud-Mac für deinen nächsten Build
Wähle Modell, Standort und Mietdauer und nutze einen dedizierten physischen Knoten für deine Entwicklungswerkzeuge. Die Verfügbarkeit wird in Echtzeit über die Konsole angezeigt.