Testen in Swift: Von der Theorie zur Praxis mit Mocks und Fixtures
Stellen Sie sich vor, Sie backen einen Kuchen und Ihnen geht das Mehl aus. Sie können nicht überprüfen, ob die Creme gelingt, bevor Sie nicht zum Supermarkt gefahren sind. Ähnlich fühlt sich ein Entwickler, der Unit-Tests für Code schreibt, der mit dem Netzwerk, einer Datenbank oder komplexen Abhängigkeiten arbeitet. Anstatt auf einen echten Server oder eine reale Datenbank zu warten, verwenden wir Mock-Objekte und Fixtures in Swift-Tests – sie sind unsere "Mehl-Ersatzstoffe", die es uns ermöglichen, isolierte Code-Stücke hier und jetzt zu testen.
In diesem Artikel werden wir nicht nur Definitionen auflisten. Wir werden analysieren, wie man Swift-Testen in der Praxis richtig anwendet, wann man Mocks und wann Stubs verwendet und wie Fixtures helfen, bei der Datenvorbereitung nicht verrückt zu werden. Los geht's!
Was sind Mock, Stub und Fixture? Ein Blick auf die Begriffe
Bevor wir Code schreiben, lassen Sie uns die Begriffe klären. In der Welt des iOS-Unit-Testens werden Mocks und Stubs oft verwechselt. Merken Sie sich eine einfache Regel:
- Stub – ein Objekt, das vordefinierte Daten zurückgibt. Es überprüft nicht, wie es aufgerufen wurde. Es gibt einfach eine Antwort zurück.
- Mock – ein Objekt, das nicht nur Daten zurückgibt, sondern auch überprüft, ob bestimmte Methoden mit welchen Parametern aufgerufen wurden. Es ist ein "Spion".
- Fixture – ein vorbereiteter Datensatz (normalerweise JSON oder eine Struktur), der in Tests verwendet wird, um eine vorhersagbare Umgebung zu schaffen.
Im Swift-Testen kombinieren wir diese Ansätze oft. Zum Beispiel laden wir eine JSON-Fixture, parsen sie und ersetzen dann die Netzwerkschicht durch einen Mock, der überprüft, ob die Anfrage an die richtige URL gesendet wurde.
Unser ersten Mock in Swift mit Protokollen schreiben
Der häufigste Weg, Mock-Objekte in Swift zu erstellen, ist die Verwendung von Protokollen. In einem echten Projekt haben Sie einen Dienst, der ins Netzwerk geht. In Tests ersetzen Sie ihn durch einen Mock.
Hier ist ein Beispiel für eine Abstraktion:
// Protokoll für die Netzwerkschichtprotocol NetworkServiceProtocol { func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void)}
// Echte Implementierung (geht ins Netzwerk)class RealNetworkService: NetworkServiceProtocol { func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) { // URLSession, Alamofire usw. }}
// Mock für Testsclass MockNetworkService: NetworkServiceProtocol { var fetchUsersCalled = false var resultToReturn: Result<[User], Error> = .success([])
func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) { fetchUsersCalled = true completion(resultToReturn) }}Jetzt können wir im Test überprüfen, ob die Methode aufgerufen wurde:
func testViewModelFetchesUsers() { let mockService = MockNetworkService() mockService.resultToReturn = .success([User(id: 1, name: "Test")])
let viewModel = UserViewModel(networkService: mockService) viewModel.loadUsers()
XCTAssertTrue(mockService.fetchUsersCalled, "Die Methode fetchUsers sollte aufgerufen worden sein")}Beachten Sie: Wir verwenden Dependency Injection in Swift – wir übergeben die Abhängigkeit über den Initialisierer. Dies ist ein Schlüsselmuster für das Testen.
Fixtures: Wie man nicht 100500 Zeilen in Tests schreibt
Wenn Sie das Parsen von JSON oder die Arbeit mit CoreData testen, benötigen Sie echte Daten. Aber sie jedes Mal im Code zu generieren, ist die Hölle. Hier kommen Fixtures in Swift-Tests zur Hilfe.
Erstellen Sie in Ihrem Testordner eine Datei users_fixture.json:
[ {"id": 1, "name": "Alice", "email": "alice@example.com"}, {"id": 2, "name": "Bob", "email": "bob@example.com"}]Jetzt