„Erst der Test – dann der Code“: Warum funktioniert das in Swift?
\n\nHaben Sie schon einmal dasselbe Modul zum dritten Mal neu geschrieben, weil „es schnell ausgeliefert werden musste“? Oder einen Bug in der Produktion gefunden, der sich beim Schreiben leicht hätte vermeiden lassen? Wenn ja – willkommen in der Welt von TDD (Test-Driven Development).
\n\nTestgetriebene Entwicklung ist nicht nur eine modische Methodik. Es ist eine Denkweise, die Sie dazu zwingt zu formulieren, wie sich der Code verhalten soll, bevor Sie ihn schreiben. In Swift, mit seiner strengen Typisierung und dem leistungsstarken Framework XCTest, wird TDD zu einem zuverlässigen Schutzschild gegen Regressionen und unerwartete Abstürze.
In diesem Artikel werden wir TDD anhand realer Beispiele durchgehen, Tests für die Netzwerkschicht und das Datenmodell schreiben und vor allem verstehen, wie man diese Praxis schmerzfrei einführt. Los geht's!
\n\nRot-Grün-Refactoring: Die Magie des Zyklus
\n\nDie Grundlage von TDD ist ein Dreischritt-Zyklus:
\n\n- \n
- Rot (Red) – Wir schreiben einen Test, der fehlschlägt (weil der Code noch nicht existiert). \n
- Grün (Green) – Wir schreiben den minimalen Code, damit der Test besteht. \n
- Refactoring (Refactor) – Wir verbessern den Code, ohne sein Verhalten zu ändern (die Tests schützen uns). \n
Klingt einfach? Probieren wir es mit Swift.
\n\nErster Test: Überprüfung des User-Modells
\n\nAngenommen, wir haben ein einfaches Benutzermodell. Wir möchten, dass es den vollständigen Namen korrekt formatiert. Zuerst – der Test:
\n\nimport XCTest\n@testable import MyApp\n\nfinal class UserTests: XCTestCase {\n \n func testFullName_GivenFirstNameAndLastName_ReturnsFormattedString() {\n // Arrange\n let user = User(firstName: "Anna", lastName: "Karenina")\n \n // Act\n let fullName = user.fullName()\n \n // Assert\n XCTAssertEqual(fullName, "Anna Karenina", "Der vollständige Name sollte aus Vor- und Nachnamen mit Leerzeichen bestehen")\n }\n}\n\n\nWir starten – der Test schlägt fehl (rot). Der Compiler beschwert sich, dass User nicht existiert. Perfekt, so ist es beabsichtigt.
Jetzt schreiben wir den minimalen Code, um den Test zu bestehen:
\n\nstruct User {\n let firstName: String\n let lastName: String\n \n func fullName() -> String {\n return "\\(firstName) \\(lastName)"\n }\n}\n\n\nWir starten – grün! Wir können zum Refactoring übergehen. Zum Beispiel die Behandlung von leeren Zeichenketten oder einem middle name hinzufügen. Aber beeilen Sie sich nicht – die Tests werden zeigen, wann dies nötig ist.
\n\nTesten der asynchronen Netzwerkschicht
\n\nEines der häufigsten Probleme in der iOS-Entwicklung ist die Netzwerkarbeit. TDD ist hier besonders nützlich, weil Sie die Logik explizit von den Netzwerkaufrufen trennen.
\n\nSchreiben eines Protokolls und eines Mock-Objekts
\n\nZuerst definieren wir, wie unser Dienst aussehen soll:
\n\nprotocol WeatherServiceProtocol {\n func fetchWeather(for city: String, completion: @escaping (Result<Weather, Error>) -> Void)\n}\n\n\nJetzt – der Test. Wir wollen nicht von einer echten API abhängen, daher erstellen wir einen Mock:
\n\nclass MockWeatherService: WeatherServiceProtocol {\n var shouldReturnError = false\n var mockWeather: Weather?\n \n func fetchWeather(for city: String, completion: @escaping (Result<Weather, Error>) -> Void) {\n if shouldReturnError {\n completion(.failure(NetworkError.serverError))\n } else if let weather = mockWeather {\n completion(.success(weather))\n }\n }\n}\n\nfinal class WeatherViewModelTests: XCTestCase {\n \n func testFetchWeather_Success_UpdatesModel() {\n // Arrange\n let mockService = MockWeatherService()\n mockService.mockWeather = Weather(temperature: 25.0, condition: "sunny")\n let viewModel = WeatherViewModel(service: mockService)\n let expectation = XCTestExpectation(description: "Warten auf das Laden des Wetters")\n \n