Tutoriel pour apprendre à faire des tests unitaires en Swift avec Xcode

Ilan Benichou

Nous allons voir ensemble un nouveau tutoriel de l'espace sous iOS/Xcode.

Le sujet : les tests unitaires !

Le but de ce tutoriel est de vous sensibiliser aux tests sous Xcode et de vous apporter les bases pour vous lancer. N'hésitez pas à écrire un commentaire si vous avez des questions ou autres.

Commentez

- Titre : Tutoriel pour apprendre à faire des tests unitaires en Swift avec Xcode

- Auteur : Ilan Benichou

- Parution : 20 novembre 2012

- Licence : Copyright ® 2017 Vincent Composieux. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Test sur un(e) Model/Classe

- III. Test sur un ViewController

- IV. Conclusion

- V. Remerciements

I - Introduction

Vous avez sûrement entendu la phrase : “tester, c'est douter”, mais dans mon équipe on aime bien rajouter : “mais douter, c'est bien”.

Alors un test unitaire, c'est quoi ?

Un test unitaire permet de :

- vérifier le bon fonctionnement d'une méthode, une classe, une portion d'un programme… ;

- garantir une non-régression de votre code ;

- se rendre compte que son code est trop long et compliqué, ce qui entraîne une refacto.

En général, un test se décompose en trois parties, suivant le schéma « AAA », qui correspond aux mots anglais « Arrange, Act, Assert », que l'on peut traduire en français par Arranger, Agir, Auditer.

- Arranger : il s'agit dans un premier temps de définir les objets, les variables nécessaires au bon fonctionnement de son test (initialiser les variables, initialiser les objets à passer en paramètres de la méthode à tester, etc.).

- Agir : il s'agit d'exécuter l'action que l'on souhaite tester (en général, exécuter la méthode que l'on veut tester, etc.).

- Auditer : vérifier que le résultat obtenu est conforme à nos attentes.

Bon, ça c'est la théorie, passons un peu à la pratique.

Sur Xcode, pour effectuer des tests, nous utiliserons le framework fourni par Apple, qui s'appelle XCTest.

Ce framework fournit pas mal de choses, mais je ne vous en dis pas plus, après on va me reprocher de spoiler.

II - Test sur un(e) Model/Classe

Tout d'abord, nous allons créer une structure Astronaute (dans un dossier Model pour faire comme si on faisait du MVC), comme ceci :

1. import Foundation

3. struct Astronaute {

4. let name: String

5. let grade: String

6. let sex: String

8. let planet: String?

10. init(name: String, grade: String, sex: String, planet: String? = nil) {

11. self.name = name

12. self.grade = grade

13. self.sex = sex

14. self.planet = planet

15. }

16. }

Comme vous pouvez le constater, un Astronaute a obligatoirement un nom, un grade et un sexe, mais n'a pas forcément de planète (c'est pas bien ça !).

On vient de créer cette structure, donc le bon réflexe à prendre, c'est de la tester tout de suite.

Alors ?

Lorsque vous créez un projet, généralement Xcode vous demande si vous désirez ajouter des unit tests (checkbox). Si vous avez coché cette case, alors vous avez un dossier finissant par Tests qui s'est créé à la racine. Supprimez le fichier généré dedans et créez un dossier Model afin de respecter l'architecture mise en place (c'est dans les bonnes pratiques).

Une fois cette étape terminée, faites un clic droit sur le dossier > New File et sélectionnez Unit Test Case Class.

Le nommage de la classe doit obligatoirement finir par Tests, soit dans notre cas AstronauteTests.

Nous allons maintenant nous attarder sur la classe générée afin de vous expliquer la base.

1. import XCTest

3. class AstronauteTests: XCTestCase {

5. override func setUp() {

6. super.setUp()

7. }

9. override func tearDown() {

10. super.tearDown()

11. }

13. func testExample() {

14. }

16. func testPerformanceExample() {

17. self.measure {

18. }

19. }

21. }

La première chose à noter est qu'on importe XCTest. Comme vous vous en doutez, ceci permet d'avoir accès au framework XCTest.

Ensuite, nous avons plusieurs méthodes que nous allons voir en détail :

- setUp(). Cette méthode est appelée avant chaque invocation de chaque méthode de tests écrits dans la classe. Pour ceux ou celles qui font des tests avec phpunit, vous avez sûrement reconnu cette méthode ;

- tearDown(). Cette méthode est appelée après l'invocation de chaque méthode de tests écrits dans la classe ;

- testExample(). Méthode créée par défaut par Xcode. Il est important de savoir que chaque méthode de test que vous allez créer doit absolument être préfixée par test ;

- testPerformanceExample(). Méthode créée par défaut par Xcode. Dans celle-ci, Xcode nous montre que nous pouvons aussi faire un test de performance. Tester la performance de votre code permet de s'assurer que les algorithmes les plus importants qui demandent un traitement particulier restent performants avec le temps.

1. import XCTest

2. @testable import tuto_xctest

4. class AstronauteTests: XCTestCase {

6. func testInitAstronaute() {

7. let astronaute = Astronaute(name: "Pepito", grade: "Amiral", sex: "Male")

9. XCTAssertEqual("Pepito", astronaute.name)

10. XCTAssertEqual("Amiral", astronaute.grade)

11. XCTAssertEqual("Male", astronaute.sex)

12. }

13. }

Rien ne vous choque ? J'ai ajouté un @testable import {nomDeMonProjet}.

En effet, sur chaque classe de test que vous allez créer, vous devrez ajouter ceci afin d'autoriser l'accès au AppDelegate notamment, mais aussi à l'ensemble des classes et méthodes créées dans votre application. Cependant, @testable donne accès seulement aux méthodes dites internes et non aux méthodes privées.

Nous allons créer notre première méthode de test. Pour ceci, nous allons tester que notre structure Astronaute initialise bien les valeurs qu'on lui passe. C'est pourquoi nous allons créer la méthode testInitAstronaute (bien évidemment, la bonne pratique est de donner un nom qui indique ce qu'on souhaite tester et son nom doit être en camelCase).

Dans cette méthode, nous initialisons dans une constante astronaute la structure Astronaute avec les paramètres obligatoires.

Pour tester que nos valeurs sont bien passées à la structure, il n'y a rien de plus simple.

Nous allons utiliser une méthode fournie par le framework XCTest. Dans notre cas, nous testerons l'égalité entre deux valeurs et nous nous servirons de la méthode XCTAssertEqual (la notion d'assert a déjà été vue plus haut) qui prend plusieurs arguments.

1. expression1 : une expression de type scalaire C ;

2. expression2 : une expression de type scalaire C ;

3. … : une description optionnelle lors d'un échec. Cette description doit être typée en String.

Cette méthode génère un échec lorsque expression1 != expression2.

Bon, on a écrit notre test, mais comment l'exécute-t-on ?

Il y a trois solutions :

1. Vous lancez tous les tests via CMD + U ;

2. Vous passez votre curseur sur le carré vide à côté du nom de la classe et celui-ci se transforme en bouton play. Ce procédé va lancer tous les tests de votre classe (cf. copie d'écran ci-dessous) ;

3. Même procédure que la solution 2, mais seulement sur la méthode que vous souhaitez tester.

Pour finir notre test, nous allons rajouter la méthode testInitAstronuateWithPlanet qui va tester l'initialisation d'un astronaute avec une planète (oui, j'aime bien mettre des noms en rapport avec Star Wars :) ).

1. func testInitAstronuateWithPlanet() {

2. let astronaute = Astronaute(name: "Skywalker", grade: "Jedi", sex: "Male", planet: "Tatooine")

4. XCTAssertEqual("Tatooine", astronaute.planet)

5. }

Bon, normalement, nous avons testé tous les cas possibles sur notre structure. Mais comment en être sûr ?

La solution : le code coverage. Il permet d'écrire le taux de code source testé d'un programme. Comment faire sous Xcode ? Cliquez sur l'icône (cf. copie d'écran ci-dessous) et cliquez sur “Edit Schema”.

Allez dans l'onglet Test et cochez la case “Gather coverage data” (cf. copie d'écran ci-dessous).

Une fois ces étapes effectuées, relancez le processus de test sur votre classe et cliquez sur l'icône qui ressemble à un message dans votre onglet à gauche puis, dans l'onglet principal, sélectionnez Code Coverage. N'oubliez pas de cocher dans cette partie la checkbox “Show Test Bundles”.

III - Test sur un ViewController

Maintenant, nous allons créer une méthode qui va changer le texte d'un label en fonction d'une condition. Voici le code d'exemple (rien de très compliqué).

1. import UIKit

3. class ViewController: UIViewController {

5. @IBOutlet weak var uiText: UILabel!

7. override func viewDidLoad() {

8. super.viewDidLoad()

9. }

11. func changeLabel(score: Int) {

12. if (score > 0) {

13. self.uiText.text = "Gagner"

15. return;

16. }

18. self.uiText.text = "Perdu"

19. }

21. }

Nous allons voir en détails ensemble comment tester ceci :

1. import XCTest

2. @testable import tuto_xctest

4. class ViewControllerTests: XCTestCase {

5. var controller:ViewController!

7. override func setUp() {

8. super.setUp()

10. let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)

11. controller = storyboard.instantiateInitialViewController() as! ViewController

12. }

14. override func tearDown() {

15. super.tearDown()

17. controller = nil

18. }

20. func testScoreIsWinChangeLabel() {

21. let _ = controller.view

22. controller.changeLabel(score: 1)

24. XCTAssertEqual("Gagner", controller.uiText.text)

25. }

27. func testScoreIsLooseChangeLabel() {

28. let _ = controller.view

29. controller.changeLabel(score: 0)

31. XCTAssertEqual("Perdu", controller.uiText.text)

32. }

34. }

Nous devons créer une variable de type ViewController afin d'accéder pour chaque méthode de test à celle-ci.

1. setUp() : (qui sera appelée avant chaque invocation de méthode de test)

1. Nous créons une constante storyboard qui va récupérer le storyboard Main (qui est par défaut votre storyboard),

2. Nous faisons appel à la méthode instantiateInitialViewController du storyboard afin d'instancier et renvoyer le controller de vue initial ;

2. tearDown() : (qui sera appelée après chaque invocation de méthode de test). Nous mettons à nil notre controller pour plus de sécurité.

3. testScoreIsWinChangeLabel() :

1. Nous souhaitons accéder au texte du label uiText de notre controller. Cependant, sans l'instruction  let _= controller.view vous allez relever une erreur, car le label sera égal à nil. Comment est-ce possible ? Quand nous avons créé notre label dans notre storyboard, celui-ci s'instancie une fois que la vue est chargée. Mais dans notre classe unitaire, la méthode loadView() n'est jamais déclenchée. Le label n'est donc pas créé et il est égal à nil. Une solution pour ce problème serait alors d'appeler controller.loadView(), mais Apple ne le recommande pas, car cela cause des problèmes de memory leaks quand les objets qui ont déjà été chargés sont de nouveau chargés. L'alternative est d'utiliser la propriété view de votre controller qui déclenchera toutes les méthodes requises.

L'utilisation d'un underscore (_) comme nom de constante a pour but de réduire le nom de la constante, car nous n'avons pas vraiment besoin de la vue. Cela dit au compilateur qu'on prétend avoir l'accès à la vue et qu'on déclenche toutes les méthodes.

4. Nous appelons la méthode concernée et nous vérifions l'assertion entre notre string et notre texte du label.

IV - Conclusion

J'espère que ce tutoriel vous a plu et qu'il vous a donné envie de faire plein de tests unitaires. J'insiste sur le fait que faire des tests est vraiment important, car :

- cela vérifie le bon fonctionnement de votre code ;

- cela cible plus facilement une erreur due à un changement de code ;

- vous y gagnerez sur le long terme.

V - Remerciements

Nous remercions Eleven labs qui nous accorde l'autorisation de publier ce tutoriel.

Nous remercions également Winjerome pour la mise au gabarit et ced pour la relecture orthographique.