Bonjour et bienvenue !
Si c’est la première fois que vous lisez un de mes articles, je me présente : je m’appelle Skander et je suis passionné par le développement iOS. Mon objectif est de partager mes découvertes et réflexions sur Swift et l’écosystème Apple, en espérant que vous y trouviez quelque chose d’intéressant et utile.
J’écris souvent mes articles lors de mes voyages en train à travers les magnifiques paysages de la France. Aujourd’hui, alors que je voyage d’Abbeville à Paris Nord pour assister au Swift Connection, je vous invite à m’accompagner dans ce nouvel article.
Les pièges des tests asynchrones en Swift : Éviter les échecs aléatoires dus aux conditions de concurrence
Récemment, l’utilisation des fonctions asynchrones en Swift est devenue de plus en plus courante, particulièrement avec l’arrivée de Swift6.
Il est toujours excitant de suivre les nouveautés technologiques et de rester à jour. Cependant, cela peut parfois être un vrai défi, surtout dans le contexte d’un projet de grande envergure.
Prenons cet exemple simple :
func toto() async {
Task {
let soso = try await manager.getFofo().value
self.soso = soso
}
}
Dès que j’ai écrit ce code, une question s’est imposée : comment tester efficacement le contenu du bloc Task
? En explorant les solutions disponibles sur internet, j’ai rapidement trouvé des exemples qui ressemblent à ceci :
func testToto() async throws {
let task = sut.toto() // ici le sut devrais déjà configuré avec un manager mock
try await task.value
XCTAssertNotNil(sut.soso)
}
Cependant, il y a un problème sous-jacent avec cette approche. Si vous n’avez pas encore lu mon article sur les Data Races en Swift 6, je vous recommande vivement de le faire avant de continuer.
Pour ceux qui l’ont déjà lu, voici la conclusion que j’en tire :
L’échec du test se produit de manière aléatoire en raison d’une condition de concurrence potentielle. Cela est fréquent lorsque l’on travaille avec du code asynchrone, comme avec les Task
de Swift. Le problème ici est que l’assertion XCTAssertNotNil
peut être exécutée avant la fin de la tâche asynchrone, entraînant des échecs intermittents.
Ce problème est encore plus amplifié lorsque plusieurs tests s’exécutent en parallèle dans votre projet, augmentant les risques d’échec liés aux conditions de course.
La solution :
Pour éviter les échecs aléatoires lors des tests, nous devons nous assurer que le bloc asynchrone a bien terminé sa tâche avant d’exécuter les assertions. Apple recommande une approche classique avec XCTestExpectation
, qui permet d’attendre la fin d’un bloc asynchrone avant de poursuivre.
func testToto() async throws {
// GIVEN
let expectation = XCTestExpectation(description: "Tâche Toto terminée")
// Simuler l'accomplissement de la tâche asynchrone dans le stub
sut.manager.totoStub = { _ in
expectation.fulfill()
return true
}
// WHEN
sut.toto()
// THEN
wait(for: [expectation], timeout: 1.0)
XCTAssertNotNil(sut.soso)
}
Alors, à votre avis, qu’est-ce qui devrait être modifié pour que notre code fonctionne sans erreur sous Swift6 ?
Bravo 🎉 ! La réponse est bien le wait(for:)
.
Avec Swift6, wait(for:)
est désormais déprécié, car il n’est plus adapté aux nouvelles APIs asynchrones introduites par async/await
. Heureusement, il existe une alternative plus moderne : await fulfillment(of:timeout:enforceOrder:)
.
Cette méthode permet d’attendre qu’une ou plusieurs attentes XCTestExpectation
soient remplies, tout en intégrant le support natif de l’asynchrone de Swift. Voici comment adapter notre test :
func testToto() async throws {
// GIVEN
let expectation = XCTestExpectation(description: "Tâche Toto terminée")
// Simuler l'accomplissement de la tâche asynchrone dans le stub
sut.manager.totoStub = { _ in
expectation.fulfill()
return true
}
// WHEN
sut.toto()
// THEN
try await fulfillment(of: [expectation], timeout: 1.0)
XCTAssertNotNil(sut.soso)
}
Voilà, c’est tout pour ce blog ! J’espère que vous avez trouvé ces explications utiles pour mieux comprendre les tests asynchrones en Swift6. Merci de m’avoir accompagné durant ce voyage à travers les défis de async/await
. Je vous donne rendez-vous très bientôt pour un autre article, et qui sait, peut-être sur un autre trajet en train à travers la France. 🚆
À bientôt pour de nouvelles aventures Swiftiennes !