Tests avancés en Swift : Unitaires, d'Intégration et E2E
Les tests ne sont pas qu'une simple vérification, mais le fondement d'un code fiable. À un niveau avancé, le développeur Swift doit non seulement écrire des tests, mais aussi construire judicieusement la pyramide de tests : des tests unitaires rapides aux scénarios complexes de bout en bout (e2e). Dans cet article, nous aborderons les techniques avancées pour les projets iOS/macOS : la gestion des dépendances, les tests d'intégration CoreData et l'automatisation de l'UI.
1. Tests unitaires : gestion des dépendances et mocks
À un niveau avancé, les tests unitaires doivent être isolés. La technique clé est l'Injection de Dépendances (DI). Au lieu de tester la couche réseau réelle ou la base de données, nous les remplaçons par des mocks.
Exemple : protocole et 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 // traitement } }}
// Mock pour les 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")) } }}Dans le test, vous créez un MockNetworkService et le passez au ViewModel. Cela permet de vérifier les scénarios d'erreur et de succès sans API réelle.
XCTestExpectation pour l'asynchrone
func testViewModelLoadDataSuccess() { let mock = MockNetworkService() let viewModel = ViewModel(networkService: mock) let expectation = self.expectation(description: "Données chargées") viewModel.loadData() // Simulation d'un appel asynchrone DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { expectation.fulfill() } waitForExpectations(timeout: 1.0) // Vérification de l'état du ViewModel}2. Tests d'intégration : CoreData et travail avec des données réelles
Les tests d'intégration vérifient l'interaction des composants. Pour CoreData, nous utilisons un conteneur en mémoire (in-memory) afin de ne pas polluer la base principale.
Configuration de CoreData en mémoire
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("Échec du chargement du store : \\(error)") } } return container }() var context: NSManagedObjectContext { return persistentContainer.viewContext }}Exemple de test d'intégration
func testSaveAndFetchUser() { let stack = CoreDataTestStack() let repository = UserRepository(context: stack.context) // Création d'un utilisateur let user = User(context: stack.context) user.name = "Utilisateur Test" user.age = 30 try? stack.context.save() // Vérification que les données sont sauvegardées let fetchRequest: NSFetchRequest<User> = User.fetchRequest() let users = try? stack.context.fetch(fetchRequest) XCTAssertEqual(users?.count, 1) XCTAssertEqual(users?.first?.name, "Utilisateur Test")}Important : après chaque test, nettoyez le contexte pour que les tests n'interfèrent pas entre eux.