Fortgeschrittenes Testen in Swift: Unit, Integration und E2E
Testen ist nicht nur eine Überprüfung, sondern das Fundament zuverlässigen Codes. Auf fortgeschrittenem Niveau muss ein Swift-Entwickler nicht nur Tests schreiben, sondern die Testpyramide geschickt aufbauen: von schnellen Unit-Tests bis hin zu komplexen End-to-End (E2E)-Szenarien. In diesem Artikel analysieren wir fortgeschrittene Techniken für iOS/macOS-Projekte: die Arbeit mit Abhängigkeiten, Integrationstests mit CoreData und UI-Automatisierung.
1. Unit-Tests: Abhängigkeitsverwaltung und Mocks
Auf fortgeschrittenem Niveau müssen Unit-Tests isoliert sein. Die Schlüsseltechnik ist Dependency Injection (DI). Anstatt die echte Netzwerkschicht oder Datenbank zu testen, ersetzen wir sie durch Mocks.
Beispiel: Protokoll und DI
protocol NetworkServiceProtocol { func fetchData(completion: @escaping (Result<String, Error>) -> Void)}
class ViewModel { private let networkService: NetworkServiceProtocol init(networkService: NetworkServiceProtocol) { self.networkService = networkService } func loadData() { networkService.fetchData { result in // Verarbeitung } }}
// Mock für Testsclass MockNetworkService: NetworkServiceProtocol { var shouldReturnError = false func fetchData(completion: @escaping (Result<String, Error>) -> Void) { if shouldReturnError { completion(.failure(NSError(domain: "test", code: -1))) } else { completion(.success("Mock data")) } }}Im Test erstellen Sie MockNetworkService und übergeben es an ViewModel. Dies ermöglicht die Überprüfung von Fehler- und Erfolgsszenarien ohne echte API.
XCTestExpectation für Asynchronität
func testViewModelLoadDataSuccess() { let mock = MockNetworkService() let viewModel = ViewModel(networkService: mock) let expectation = self.expectation(description: "Data loaded") viewModel.loadData() // Simuliere asynchronen Aufruf DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { expectation.fulfill() } waitForExpectations(timeout: 1.0) // Überprüfe den Zustand des ViewModel}2. Integrationstests: CoreData und Arbeit mit echten Daten
Integrationstests überprüfen das Zusammenspiel von Komponenten. Für CoreData verwenden wir einen In-Memory-Container, um die Hauptdatenbank nicht zu verschmutzen.
Einrichtung von In-Memory CoreData
class CoreDataTestStack { lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "MyModel") let description = NSPersistentStoreDescription() description.type = NSInMemoryStoreType container.persistentStoreDescriptions = [description] container.loadPersistentStores { _, error in if let error = error { fatalError("Failed to load store: \\(error)") } } return container }() var context: NSManagedObjectContext { return persistentContainer.viewContext }}Beispiel eines Integrationstests
func testSaveAndFetchUser() { let stack = CoreDataTestStack() let repository = UserRepository(context: stack.context) // Erstelle einen Benutzer let user = User(context: stack.context) user.name = "Test User" user.age = 30 try? stack.context.save() // Überprüfe, ob die Daten gespeichert wurden let fetchRequest: NSFetchRequest<User> = User.fetchRequest() let users = try? stack.context.fetch(fetchRequest) XCTAssertEqual(users?.count, 1) XCTAssertEqual(users?.first?.name, "Test User")}Wichtig: Bereinigen Sie nach jedem Test den Kontext, damit die Tests sich nicht gegenseitig beeinflussen.