Tests en Swift : de la théorie à la pratique avec les mocks et les fixtures
Imaginez que vous préparez un gâteau et que vous n'avez plus de farine. Vous ne pouvez pas vérifier si la crème est réussie avant d'aller au magasin. C'est à peu près la même sensation pour un développeur qui écrit des tests unitaires pour du code interagissant avec le réseau, une base de données ou des dépendances complexes. Au lieu d'attendre un vrai serveur ou une base de données réelle, nous utilisons des objets mock et des fixtures dans les tests Swift — ce sont nos "substituts de farine" qui permettent de tester des morceaux de code isolés ici et maintenant.
Dans cet article, nous ne nous contenterons pas d'énumérer des définitions. Nous verrons comment appliquer correctement les tests Swift en pratique, quand utiliser les mocks et quand utiliser les stubs, et comment les fixtures aident à ne pas perdre la raison lors de la préparation des données. C'est parti !
Qu'est-ce que mock, stub et fixture ? Clarifions les termes
Avant d'écrire du code, mettons-nous d'accord sur les concepts. Dans le monde des tests unitaires iOS, on confond souvent les mocks et les stubs. Retenez cette règle simple :
- Stub (bouchon) — un objet qui renvoie des données prédéfinies. Il ne vérifie pas comment il a été appelé. Il se contente de fournir une réponse.
- Mock (simulation) — un objet qui non seulement renvoie des données, mais vérifie également si certaines méthodes ont été appelées et avec quels paramètres. C'est un "espion".
- Fixture (fixation) — un ensemble de données préparé à l'avance (généralement du JSON ou une structure) utilisé dans les tests pour créer un environnement prévisible.
Dans les tests Swift, nous combinons souvent ces approches. Par exemple, nous chargeons une fixture JSON, nous l'analysons, puis nous remplaçons la couche réseau par un mock qui vérifie que la requête a été envoyée à la bonne URL.
Écrire notre premier mock en Swift avec des protocoles
La façon la plus courante de créer des objets mock en Swift est d'utiliser des protocoles. Dans un projet réel, vous avez un service qui accède au réseau. Dans les tests, vous le remplacez par un mock.
Voici un exemple d'abstraction :
// Protocole pour la couche réseauprotocol NetworkServiceProtocol { func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void)}
// Implémentation réelle (accède au réseau)class RealNetworkService: NetworkServiceProtocol { func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) { // URLSession, Alamofire, etc. }}
// Mock pour les testsclass MockNetworkService: NetworkServiceProtocol { var fetchUsersCalled = false var resultToReturn: Result<[User], Error> = .success([])
func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) { fetchUsersCalled = true completion(resultToReturn) }}Maintenant, dans le test, nous pouvons vérifier que la méthode a été appelée :
func testViewModelFetchesUsers() { let mockService = MockNetworkService() mockService.resultToReturn = .success([User(id: 1, name: "Test")])
let viewModel = UserViewModel(networkService: mockService) viewModel.loadUsers()
XCTAssertTrue(mockService.fetchUsersCalled, "La méthode fetchUsers doit être appelée")}Remarquez que nous utilisons l'injection de dépendances Swift — nous passons la dépendance via l'initialiseur. C'est un modèle clé pour les tests.
Fixtures : comment ne pas écrire 100500 lignes dans les tests
Lorsque vous testez l'analyse JSON ou le travail avec CoreData, vous avez besoin de données réelles. Mais les générer à chaque fois dans le code est un enfer. C'est là que les fixtures dans les tests Swift viennent à la rescousse.
Créez un fichier users_fixture.json dans le dossier de test :
[ {"id": 1, "name": "Alice", "email": "alice@example.com"}, {"id": 2, "name": "Bob", "email": "bob@example.com"}]Maintenant