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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
import
Foundation
struct
Astronaute
{
let
name
:
String
let
grade
:
String
let
sex
:
String
let
planet
:
String
?
init
(
name
:
String
,
grade
:
String
,
sex
:
String
,
planet
:
String
?
=
nil
)
{
self
.
name
=
name
self
.
grade
=
grade
self
.
sex
=
sex
self
.
planet
=
planet
}
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
import
XCTest
class
AstronauteTests
:
XCTestCase
{
override
func
setUp
()
{
super
.
setUp
()
}
override
func
tearDown
()
{
super
.
tearDown
()
}
func
testExample
()
{
}
func
testPerformanceExample
()
{
self
.
measure
{
}
}
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
import
XCTest
@testable
import
tuto_xctest
class
AstronauteTests
:
XCTestCase
{
func
testInitAstronaute
()
{
let
astronaute
=
Astronaute
(
name
:
"Pepito"
,
grade
:
"Amiral"
,
sex
:
"Male"
)
XCTAssertEqual
(
"Pepito"
,
astronaute
.
name
)
XCTAssertEqual
(
"Amiral"
,
astronaute
.
grade
)
XCTAssertEqual
(
"Male"
,
astronaute
.
sex
)
}
}
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.
- expression1 : une expression de type scalaire C ;
- expression2 : une expression de type scalaire C ;
- … : 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 :
- Vous lancez tous les tests via CMD + U ;
- 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) ;
- 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 :) ).
2.
3.
4.
5.
func
testInitAstronuateWithPlanet
()
{
let
astronaute
=
Astronaute
(
name
:
"Skywalker"
,
grade
:
"Jedi"
,
sex
:
"Male"
,
planet
:
"Tatooine"
)
XCTAssertEqual
(
"Tatooine"
,
astronaute
.
planet
)
}
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é).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
import
UIKit
class
ViewController
:
UIViewController
{
@IBOutlet
weak
var
uiText
:
UILabel
!
override
func
viewDidLoad
()
{
super
.
viewDidLoad
()
}
func
changeLabel
(
score
:
Int
)
{
if
(
score
>
0
)
{
self
.
uiText
.
text
=
"Gagner"
return
;
}
self
.
uiText
.
text
=
"Perdu"
}
}
Nous allons voir en détails ensemble comment tester ceci :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
import
XCTest
@testable
import
tuto_xctest
class
ViewControllerTests
:
XCTestCase
{
var
controller
:
ViewController
!
override
func
setUp
()
{
super
.
setUp
()
let
storyboard
=
UIStoryboard
(
name
:
"Main"
,
bundle
:
Bundle
.
main
)
controller
=
storyboard
.
instantiateInitialViewController
()
as
!
ViewController
}
override
func
tearDown
()
{
super
.
tearDown
()
controller
=
nil
}
func
testScoreIsWinChangeLabel
()
{
let
_
=
controller
.
view
controller
.
changeLabel
(
score
:
1
)
XCTAssertEqual
(
"Gagner"
,
controller
.
uiText
.
text
)
}
func
testScoreIsLooseChangeLabel
()
{
let
_
=
controller
.
view
controller
.
changeLabel
(
score
:
0
)
XCTAssertEqual
(
"Perdu"
,
controller
.
uiText
.
text
)
}
}
Nous devons créer une variable de type ViewController afin d'accéder pour chaque méthode de test à celle-ci.
-
setUp() : (qui sera appelée avant chaque invocation de méthode de test)
- Nous créons une constante storyboard qui va récupérer le storyboard Main (qui est par défaut votre storyboard),
- Nous faisons appel à la méthode instantiateInitialViewController du storyboard afin d'instancier et renvoyer le controller de vue initial ;
- tearDown() : (qui sera appelée après chaque invocation de méthode de test). Nous mettons à nil notre controller pour plus de sécurité.
-
testScoreIsWinChangeLabel() :
-
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.
-
- 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.