Introduction : Pourquoi les tests sont le fondement d'une application Swift fiable
Le développement pour iOS et macOS nécessite non seulement une belle interface, mais aussi une logique stable. Les tests en Swift ne sont pas une option, mais une nécessité. Sans tests, même un petit changement peut casser une fonctionnalité qui fonctionnait depuis des années. Dans cet article, nous allons aborder trois niveaux de test : unit (tests unitaires), integration (tests d'intégration) et e2e (tests de bout en bout). Vous apprendrez à les configurer, les écrire et les exécuter correctement sur les plateformes Apple. À la fin, des conseils pratiques pour organiser l'architecture des tests.
Niveau 1 : Tests unitaires — les bases fondamentales
Les tests unitaires vérifient des fonctions, méthodes ou classes individuelles de manière isolée. En Swift, le framework principal est XCTest. Il est intégré à Xcode, ne nécessite pas de dépendances tierces et est parfait pour tester la logique métier.
Comment écrire votre premier test unitaire
Créez une nouvelle cible de test unitaire dans Xcode (File → New → Target → Unit Testing Bundle). Ajoutez ensuite une classe de test :
import XCTest
@testable import MyApp
final class CalculatorTests: XCTestCase {
var sut: Calculator!
override func setUpWithError() throws {
sut = Calculator()
}
override func tearDownWithError() throws {
sut = nil
}
func testAddition() {
// Given
let a = 5
let b = 3
// When
let result = sut.add(a, b)
// Then
XCTAssertEqual(result, 8, "L'addition doit être correcte")
}
}
Bonnes pratiques des tests unitaires
- Isolation : chaque test ne teste qu'une seule unité de code. Utilisez
setUpettearDownpour réinitialiser l'état. - Nommage : utilisez le modèle
test[Méthode]_[Condition]_[RésultatAttendu]. - Mocks et stubs : pour les dépendances externes (réseau, base de données), utilisez des protocoles et remplacez-les dans les tests.
- Couverture : visez 70-80% de couverture de code, mais ne cherchez pas les 100% — testez les chemins critiques.
Exemple avec un mock
protocol NetworkServiceProtocol {
func fetchData(completion: @escaping (Result<Data, Error>) -> Void)
}
class MockNetworkService: NetworkServiceProtocol {
var shouldReturnError = false
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
if shouldReturnError {
completion(.failure(NSError(domain: "test", code: 1)))
} else {
completion(.success(Data()))
}
}
}
func testFetchDataSuccess() {
let mock = MockNetworkService()
let viewModel = ViewModel(networkService: mock)
mock.shouldReturnError = false
viewModel.loadData()
XCTAssertTrue(viewModel.isDataLoaded)
}
Niveau 2 : Tests d'intégration — vérification des interactions
Les tests d'intégration vérifient comment différents modules du système fonctionnent ensemble : base de données + réseau, UI + logique métier, plusieurs services. En Swift, on utilise XCTest avec des dépendances réelles (ou in-memory).
Scénario typique : Core Data + Network
import XCTest
@testable import MyApp
class UserServiceIntegrationTests: XCTestCase {
var coreDataStack: CoreDataStack!
var networkService: NetworkService!
var userService: UserService!
override func setUp() {
coreDataStack = CoreDataStack(inMemory: true) // in-memory pour les tests
networkService = NetworkService()
userService = UserService(coreDataStack: coreDataStack, networkService: networkService)
}
func testSaveUserFromNetwork() {
// Créer une attente pour la requête asynchrone
let expectation = self.expe