« D'abord le test — ensuite le code » : pourquoi ça fonctionne en Swift ?
\n\nAvez-vous déjà réécrit le même module pour la troisième fois parce qu'il fallait « le lancer en urgence » ? Ou attrapé un bug en production qu'il aurait été plus simple d'éviter lors de l'écriture ? Si oui — bienvenue dans le monde du TDD (Test-Driven Development).
\n\nLe développement piloté par les tests n'est pas qu'une simple méthodologie à la mode. C'est une façon de penser qui vous oblige à formuler comment le code doit se comporter avant même de l'écrire. En Swift, avec son typage strict et son puissant framework XCTest, le TDD devient un bouclier fiable contre les régressions et les crashs inattendus.
Dans cet article, nous allons décortiquer le TDD avec des exemples concrets, écrire des tests pour la couche réseau et le modèle de données, et surtout — comprendre comment adopter cette pratique sans douleur. C'est parti !
\n\nRouge-Vert-Refactorisation : la magie du cycle
\n\nLa base du TDD est un cycle en trois étapes :
\n\n- \n
- Rouge (Red) — on écrit un test qui échoue (parce que le code n'existe pas encore). \n
- Vert (Green) — on écrit le code minimal pour que le test passe. \n
- Refactorisation (Refactor) — on améliore le code sans changer son comportement (les tests protègent). \n
Cela semble simple ? Essayons en Swift.
\n\nPremier test : vérifier le modèle User
\n\nSupposons que nous ayons un modèle utilisateur simple. Nous voulons qu'il formate correctement le nom complet. D'abord — le test :
\n\nimport XCTest\n@testable import MonApp\n\nfinal class UserTests: XCTestCase {\n \n func testFullName_GivenFirstNameAndLastName_ReturnsFormattedString() {\n // Arrange\n let user = User(firstName: "Anna", lastName: "Karénine")\n \n // Act\n let fullName = user.fullName()\n \n // Assert\n XCTAssertEqual(fullName, "Anna Karénine", "Le nom complet doit être composé du prénom et du nom séparés par un espace")\n }\n}\n\n\nOn lance — le test échoue (rouge). Le compilateur râle car User n'existe pas. Parfait, c'est prévu.
Maintenant, on écrit le code minimal pour faire passer le test :
\n\nstruct User {\n let firstName: String\n let lastName: String\n \n func fullName() -> String {\n return "\\(firstName) \\(lastName)"\n }\n}\n\n\nOn lance — vert ! On peut passer à la refactorisation. Par exemple, ajouter la gestion des chaînes vides ou du deuxième prénom. Mais ne vous précipitez pas — les tests vous indiqueront quand c'est nécessaire.
\n\nTester la couche réseau asynchrone
\n\nL'un des problèmes les plus fréquents en développement iOS — le travail avec le réseau. Le TDD est particulièrement utile ici car vous séparez clairement la logique des appels réseau.
\n\nÉcrire le protocole et l'objet mock
\n\nD'abord, définissons à quoi notre service doit ressembler :
\n\nprotocol WeatherServiceProtocol {\n func fetchWeather(for city: String, completion: @escaping (Result<Weather, Error>) -> Void)\n}\n\n\nMaintenant — le test. Nous ne voulons pas dépendre d'une API réelle, donc nous créons un 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: "ensoleillé")\n let viewModel = WeatherViewModel(service: mockService)\n let expectation = XCTestExpectation(description: "Attente du chargement de la météo")\n \n