C'est bien beau de s'amuser à faire des petits tests pour tester le fonctionnement des tests. Mais trop de tests tuent les tests et vous allez finir par les détester. :D
Alors, rentrons dans le vif du sujet et testons notre classe Game
!
Importer l'application
Comme nous l'avons vu dans le projet précédent, les tests et l'application coexistent dans un même projet, mais se trouvent dans des targets séparées. Cela veut dire qu'ils ne sont pas dans le même module. Donc si vous essayez d'utiliser la classe Game ou toute autre classe dans les tests, Swift va vous signaler qu'il ne connaît pas cette classe.
Pour cela, pas de panique ! Il nous suffit d'importer le module qui correspond à notre application. Il a le nom de la target correspondante, à savoir : JeuSetMatch
.
Vous pouvez écrire en haut de votre fichier test :
import JeuSetMatch
Désormais le module de l'application est importé dans votre fichier de tests.
Mais ce n'est pas suffisant ! Pour comprendre pourquoi, faisons un petit rappel du contrôle d'accès.
Il y a 4 niveaux que je vous résume dans ce schéma :
Le niveau par défaut, c'est le niveau interne. À ce niveau, la portée des classes et de leurs membres est limitée au module qui les contient. Donc en l'occurrence, notre classe Game
et toutes ses méthodes ne sont pas disponibles en dehors de l'application. Donc lorsqu'on importe le module JeuSetMatch
du côté des tests, nous n'avons pas accès à Game
et aux autres classes du modèle, car nous sommes dans un autre module.
Pour y remédier, nous avons deux solutions :
Modifier les classes et les méthodes pour qu'elles soient toutes publiques. D'une part, ça va nous prendre un peu de temps et d'autre part ce n'est pas très sécurisé.
Utilisez le décorateur
@testable
. Et c'est ce que nous allons faire !
Le décorateur @testable
se place avant l'import d'un module. Il permet de faire comme si le fichier de test faisait partie du même module. Ainsi, le fichier de test a accès à toutes les classes et leurs membres à partir du niveau interne.
Écrivez donc ceci ans votre code :
@testable import JeuSetMatch
Nous avons maintenant accès à la classe Game
et nous allons pouvoir la tester !
Écriture du premier test
Vous pouvez supprimer le testExemple
, nous allons écrire à la place notre premier vrai test. Et nous n'allons pas l'appeler n'importe comment !
Pour nommer vos tests, je vous suggère d'utiliser une technique bien pratique : le Behavior Driven Development (ou développement motivé par le comportement). En BDD, vos noms de tests vont être divisés en trois parties :
Given : Etant donné que... [Situation de départ]
When : Quand... [Action]
Then : Alors... [Situation d'arrivée]
Par exemple, si on devait écrire le nom d'un test qui contrôle le fonctionnement d'un like, on écrirait :
GivenPostHasZeroLike_WhenPostIsLiked_ThenPostHasOneLike
// ETANT DONNÉ QUE le poste n'a pas de like
// QUAND le poste est liké
// ALORS le poste a un like
Faisons cela avec notre premier test de la classe Game
, on va tester que le score du joueur 1 passe bien de 0 à 15 lorsqu'il gagne le premier point :
func testGivenScoreIsNull_WhenIncrementingPlayer1Score_ThenScoreShouldBeFifteen() {
}
Il ne nous reste plus qu'à coder ce qui est dit dans le nom du test :
func testGivenScoreIsNull_WhenIncrementingPlayer1Score_ThenScoreShouldBeFifteen() {
// 1
let game = Game()
// 2
game.incrementScore(forPlayer: .one)
// 3
XCTAssert(game.scores[.one]! == 15)
XCTAssert(game.scores[.two]! == 0)
}
Voyons un peu ce que je viens de rédiger :
Situation de départ : un jeu avec un score nul.
Action : le point est gagné par le joueur 1.
Situation d'arrivée : le score du joueur 1 vaut 15, celui du joueur 2 vaut 0.
Nous avons rédigé notre premier test ! Vous pouvez le lancer et constater que cela fonctionne !
Écriture du deuxième test
Passons à la suite ! Nous allons maintenant tester qu'au deuxième point gagné, le score passe de 15 à 30. Essayez de le faire tout seul !
Voici la correction :
func testGivenScoreIsFifteen_WhenIncrementingPlayer1Score_ThenScoreShouldBeThirty() {
let game = Game()
game.scores[.one] = 15
game.incrementScore(forPlayer: .one)
XCTAssert(game.scores[.one]! == 30)
XCTAssert(game.scores[.two]! == 0)
}
C'est quasiment la même chose. On a juste rajouté la deuxième ligne pour que le score démarre à 15 points.
Il y a quelque chose qui me choque quand même. La première ligne est rigoureusement identique dans les deux tests ! Or un bon développeur n'aime pas se répéter ! Il faut factoriser. Je vous propose d'extraire cette ligne et d'en faire une propriété :
class GameTestCase: XCTestCase {
let game = Game()
func testGivenScoreIsNull_WhenIncrementingPlayer1Score_ThenScoreShouldBeFifteen() {
game.incrementScore(forPlayer: .one)
XCTAssert(game.scores[.one]! == 15)
XCTAssert(game.scores[.two]! == 0)
}
func testGivenScoreIsFifteen_WhenIncrementingPlayer1Score_ThenScoreShouldBeThirty() {
game.scores[.one] = 15
game.incrementScore(forPlayer: .one)
XCTAssert(game.scores[.one]! == 30)
XCTAssert(game.scores[.two]! == 0)
}
}
C'est mieux comme ça ! Le seul problème, c'est que nos tests ne sont plus indépendants les uns des autres. Or ils doivent l'être, car un test unitaire s'occupe d'une petite unité de code. Si ce test porte avec lui l'historique des tests précédents, ça ne fonctionne plus !
Pour y remédier, nous allons utiliser la méthode setup
. C'est une méthode de XCTestCase
qui permet de faire un peu de préparation. Cette méthode est rappelée avant chaque test. Donc dans notre cas, elle va être appelée deux fois.
Je vous propose de l'utiliser pour initialiser notre propriété game
. Ainsi avant chaque début de test, la propriété sera réinitialisée :
var game: Game!
override func setUp() {
super.setUp()
game = Game()
}
Vous pouvez relancer vos tests et vérifier qu'ils réussissent toujours !
Écriture du troisième test
Vous commencez à connaître la musique. On va tester maintenant que le troisième point gagné fait passer le score de 30 à 40. À vous de jouer !
Voici la correction :
func testGivenScoreIsThirty_WhenIncrementingPlayer1Score_ThenScoreShouldBeForty() {
game.scores[.one] = 30
game.incrementScore(forPlayer: .one)
XCTAssert(game.scores[.one]! == 40)
XCTAssert(game.scores[.two]! == 0)
}
Pas grand chose de nouveau ici. Mais il y a encore quelque chose qui me chiffonne... La première ligne ressemble beaucoup à la première ligne du test précédent. Est-ce qu'on ne pourrait pas factoriser ?
On va créer une méthode pour factoriser cette ligne. Eh oui ! On est dans une classe, on peut écrire toutes les méthodes qu'on veut. On n'est pas obligé d'écrire que des tests. Il suffit qu'elles ne commencent pas par le mot test
.
Allons-y :
func setPlayerOneScore(_ score: Int) {
game.scores[.one] = score
}
Et dans nos tests on peut maintenant écrire :
setPlayerOneScore(15) // Pour le deuxième test
setPlayerOneScore(30) // Pour le troisième test
Écriture du quatrième test
Il ne nous reste plus qu'un test à écrire. Lorsque le score du joueur 1 vaut 40 et qu'il gagne le point, le jeu doit être terminé et gagné par le joueur 1. Vous voulez essayer de le faire seul ? Allez-y !
Voici la correction :
func testGivenScoreIsForty_WhenIncrementingPlayer1Score_ThenGameIsOverAndWonByPlayer1() {
setPlayerOneScore(40)
game.incrementScore(forPlayer: .one)
XCTAssertEqual(game.winner, .one)
XCTAssertTrue(game.isOver)
}
J'ai pris la liberté de vous montrer ici deux variantes de la méthode XCTAssert
:
XCTAssertEqual
: Très utile, cette variante prend deux paramètres et le test réussit s'ils sont égaux. L'opposéXCTAssertNotEqual
existe également.XCTAssertTrue
: Elle fonctionne exactement commeXCTAssert
, mais elle est plus lisible. L'opposéXCTAssertFalse
existe également.
Et voilà ! Notre classe Game
est intégralement testée ! Bravo !
Exercice
Je vous propose d'essayer de rédiger les tests de la classe Set
. Il y a trois tests à créer :
Testez que la propriété calculée
scores
fonctionne bien. Les scores doivent correspondre au nombre de jeux gagnés par chaque joueur.Testez que la propriété calculée
winner
fonctionne bien. Elle doit être ànil
, si la partie n'est pas terminée.Testez que si le joueur 1 a gagné 6 jeux, le set est terminé et gagné par le joueur 1.
Vous pouvez retrouver la correction ici. Bon courage, vous pouvez le faire !
En résumé
On écrit les noms des tests en Behavior Design Development et on sépare le nom en trois parties : Given, When, Then.
La méthode
setup
est rappelée avant chaque test. Elle permet de faire une initialisation.Vous devez traiter vos tests comme le reste du code. Donc n'hésitez pas à les factoriser en créant des méthodes dès que besoin.
Il existe plusieurs variantes de
XCTAssert
. Je vous invite à les utiliser le plus possible pour améliorer la lisibilité de vos tests.
Maintenant que vous avez rédigé vos premiers tests et que vous avez compris la logique d'un test unitaire, nous allons parler de l'utilité de créer et maintenir vos tests unitaires ! Rendez-vous au prochain chapitre !