XXV. Chapitre 25 : Un blog : les tests unitaires▲
Nous allons voir dans ce chapitre un aspect important d'une application : les tests unitaires. Comme il est humainement impossible d'écrire un code sans erreur, il faut trouver un moyen efficace de les détecter. La méthode traditionnelle consiste à jouer l'utilisateur et à essayer une à une toutes les fonctionnalités. C'est long, laborieux et pas forcément complet. Heureusement il existe une autre possibilité qui consiste à automatiser ces tests. C'est justement le but des tests unitaires.
XXV-A. C'est quoi un test unitaire ?▲
Un test unitaire c'est un bout de code qui va vérifier qu'une fonctionnalité de votre application correspond bien au résultat attendu. En gros du code qui vérifie un autre code. Si le résultat est correct tout va bien, sinon vous recevez un message qui vous indique le problème. Le fait de diviser vos tests en petites unités présente de nombreux avantages : les résultats sont bien organisés et faciles à lire, on a une progression séquentielle des tests, on peut rendre certains tests dépendants d'autres tests…
XXV-A-1. Que faut-il pour faire des tests ?▲
Laravel utilise le framework PHPUnit pour effectuer les tests. Ce framework est devenu de fait le standard dans ce domaine. Il est à la fois puissant et simple à utiliser. D'autre part Laravel 4 a été conçu dans le souci de rendre les tests faciles à réaliser avec PHPUnit. Pour utiliser ce framework vous devez l'installer. Il y a plusieurs possibilités mais la plus simple consiste à l'utiliser sous la forme d'un fichier phar que vous pouvez récupérer ici. Il vous suffit de placer ce fichier à la racine de votre application et il est disponible ! Laravel utilise aussi des composants de Symfony (HttpKernel, DomCrawler, et BrowserKit). D'autre part vous pouvez utiliser également Mockery. Dans ce chapitre, je vais me contenter de choses simples sans faire trop d'appels à ces composants. Ce choix n'est pas innocent et a de fortes implications, en particulier concernant Mockery, car ce dernier permet de simuler le fonctionnement de classes et d'ainsi pouvoir réellement isoler les tests.
XXV-A-2. Principes de base de PHPUnit▲
Je vous conseille la lecture de la documentation au style très agréable et précis. Les éléments importants sont :
- un test pour un classe MaClasse va dans une classe MaClasseTest ;
- un test est une méthode publique dont le nom commence par test ;
- dans la méthode du test on utilise des méthodes d'assertion (par exemple assertTrue()) pour affirmer que le test est correct ;
- les tests sont à placer dans une architecture de dossiers (dans le répertoire app/tests pour Laravel) qui correspond à celle des classes à tester (notez qu'il y a à la racine de l'application un fichier phpunit.xml qui renseigne entre autres sur le dossier des tests) ;
- chaque test se fait dans un environnement nouveau avec un état connu, ce qui se nomme fixture du test. Lors de l'utilisation de tests, Laravel se met automatiquement en mode test (testing). Il y a un dossier app/config/testing destiné à recevoir la configuration pour les tests (cache, base…).
XXV-A-3. Configurer la base de données pour les tests▲
Il n'est pas vraiment conseillé de faire les tests sur la base qui vous sert en production. Vous avez la possibilité de créer un fichier de configuration app/config/testing/database.php destiné à recevoir la configuration de la base pour la situation de test, et uniquement pour elle. La façon la plus efficace de procéder est d'utiliser SQLite en mode memory. Autrement dit la base est créée en mémoire vive et disparaît à l'issue du test. On y gagne en rapidité d'exécution. Mais il faut évidemment avoir les bonnes migrations et seeds pour générer la base et la remplir avec des données de test. Voici le contenu de ce fichier pour nos tests :
En mode test la base MySQL est ignorée et c'est une base SQLite qui est créée en mémoire vive.
XXV-A-4. Les fichier TestCase▲
Le fichier app/test/TestCase contient l'initialisation de vos tests, si vous l'ouvrez vous trouvez cela :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
class TestCase extends
Illuminate\Foundation\Testing\TestCase {
/**
* Creates the application.
*
*
@return
Symfony\Component\HttpKernel\HttpKernelInterface
*/
public
function
createApplication()
{
$unitTesting
=
true
;
$testEnvironment
=
'testing'
;
return
require __DIR__.
'/../../bootstrap/start.php'
;
}
}
On voit qu'une application est créée et que l'environnement se met en mode test ici. On va évidemment conserver ce code indispensable mais il nous faut créer la base, la remplir et prendre une dernière précaution : éviter d'envoyer des emails et plutôt générer un log. Voici le petit ajout :
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.
class TestCase extends
Illuminate\Foundation\Testing\TestCase {
/**
* Préparation avant les tests
*/
public
function
setUp()
{
parent
::
setUp();
Artisan::
call('migrate'
);
$this
->
seed();
Mail::
pretend(true
);
}
/**
* Creates the application.
*
*
@return
Symfony\Component\HttpKernel\HttpKernelInterface
*/
public
function
createApplication()
{
$unitTesting
=
true
;
$testEnvironment
=
'testing'
;
return
require __DIR__.
'/../../bootstrap/start.php'
;
}
}
La méthode setUp est appelée avant chaque test unitaire. Maintenant nous sommes sûrs d'avoir une base de données fonctionnelle pour nos tests sans interférer sur la base de production .
Vous avez également un fichier app/test/ExampleTest.php que vous devez supprimer.
XXV-A-5. Architecture des dossiers▲
Voici l'architecture avec nos contrôleurs :
Nous aurons donc la même architecture pour les tests :
Et voici le contenu initial pour ces fichiers :
class HomeControllerTest extends
TestCase {}
class AuthControllerTest extends
TestCase {}
class GuestControllerTest extends
TestCase {}
Remarquez que ces classes dérivent de TestCase et qu'elles respectent la convention de nommage en finissant par Test. Pour le moment créez seulement le premier, pour éviter de recevoir un message de la part de PHPUnit dans la suite de notre démarche.
XXV-A-6. Notre premier test▲
Il faut bien comprendre la philosophie des tests. Vous faites une action et vous attendez un certain résultat. Prenons cette action du contrôleur HomeController :
Cette action est activée par cette route :
Autrement dit avec l'URL http://localhost/blog/public on s'attend à voir apparaître la page d'accueil du blog. Nous allons créer un test pour vérifier que ça fonctionne bien :
2.
3.
4.
5.
6.
7.
8.
class HomeControllerTest extends
TestCase {
public
function
testAccueil()
{
$response
=
$this
->
call('GET'
,
'/'
);
$this
->
assertResponseOk();
}
}
Le test s'appelle testAccueil (il commence forcément par test). La première instruction génère la requête et récupère la réponse. La seconde instruction est une assertion qui vérifie qu'on reçoit une réponse correcte. Lançons maintenant PHPUnit :
On trouve logiquement un test et une assertion, avec un résultat positif. Que nous apprend ce test ? Que le chemin est bon, qu'on renvoie une réponse correcte, mais on n'a aucune idée du contenu de la réponse. C'est ici qu'il faut savoir ce qu'on désire réellement tester et jusqu'où aller. Soyons un peu curieux et voyons ce que nous offre cette réponse :
On se retrouve avec le contenu de la page HTML retournée. Ce qui peut être utile.
Nous savons aussi que nous transmettons trois paramètres à la vue, et nous pouvons tester cette transmission ainsi :
Cette fois nous avons quatre assertions, une pour chaque paramètre et une pour la réponse.
Vous pouvez aussi utiliser le crawler (robot d'indexation) pour explorer le DOM, et filtrer des informations :
Vous avez la documentation du crawler ici.
XXV-A-7. Les tests pour HomeController▲
XXV-A-7-a. Actions « accueil », « categorie » et « article »▲
Maintenant que nous nous sommes échauffés, passons en revue les actions du contrôleur HomeController pour écrire les tests correspondants. Il y a trois actions de même nature pour lesquelles nous allons nous contenter de vérifier la réponse correcte :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
public function testAccueil()
{
$response
=
$this
->
call('
GET
'
,
'
/
'
);
$this
->
assertResponseOk();
}
public function testCategorie()
{
$response
=
$this
->
call('
GET
'
,
'
cat/1
'
);
$this
->
assertResponseOk();
}
public function testArticle()
{
$response
=
$this
->
call('
GET
'
,
'
art/1/1
'
);
$this
->
assertResponseOk();
}
XXV-A-7-b. Action « find »▲
Maintenant il nous faut réfléchir à une action un peu particulière, celle de la recherche à partir du formulaire prévu à cet effet. Il nous faut définir deux tests : un avec le résultat de recherche avec des articles trouvés ou pas, et l'autre négatif en cas de champ de recherche vide. Voyons pour le test positif. Nous devons trouver un terme de recherche, simuler l'envoi du formulaire et voir si la réponse est correcte :
Voyons un peu où nous en sommes :
Voyons à présent le cas du champ vide :
Cette fois on envoie un formulaire sans le champ. Vous voyez l'utilisation de la méthode assertRedirectedTo pour tester la redirection et assertSessionHas pour vérifier la présence d'une information de session.
XXV-A-7-c. Action « comment »▲
Il ne nous manque plus qu'à tester l'action « comment » pour en finir avec ce contrôleur :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
public function testComment()
{
// Authentification
$this
->
be(User::
find(1
));
// Envoi du formulaire
$response
=
$this
->
call('
POST
'
,
'
comment
'
,
array(
'
title
'
=>
'
titre à tester
'
,
'
comment
'
=>
'
commentaire
'
,
'
id_art
'
=>
1
,
'
id_cat
'
=>
1
)
);
// Assertions
$this
->
assertEquals(1
,
Comment::
where('
title
'
,
'
titre à tester
'
)->
count());
$this
->
assertRedirectedTo('
art/1/1
'
);
}
Notez qu'on doit procéder dans un premier temps à l'authentification avec la méthode be parce que seuls les utilisateurs connectés peuvent laisser un commentaire. On voit aussi apparaître la méthode assertEquals qui permet de tester une égalité, ça nous sert pour vérifier que l'information a bien été mémorisée dans la base.
XXV-A-8. Les tests pour AuthController▲
Ici ça ira vite étant donné qu'il n'y a que l'action « logout » :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
class AuthControllerTest extends
TestCase {
public
function
testLogout()
{
// Authentification
$this
->
be(User::
find(1
));
$this
->
assertTrue
(Auth::
check());
// Requête
$response
=
$this
->
call('GET'
,
'auth/logout'
);
// Assertions
$this
->
assertTrue
(Auth::
guest());
$this
->
assertRedirectedToRoute('accueil'
);
}
}
On commence par authentifier l'utilisateur avec la méthode be. On vérifie que c'est bien fait. Ensuite on génère la requête et on vérifie que l'utilisateur n'est plus authentifié. Il ne nous reste plus qu'à vérifier la redirection. Voyons où nous en sommes :
XXV-A-9. Les tests pour GuestController▲
Ici nous avons un peu plus de travail. Mais vous avez compris le principe, je vous livre donc le code sans le commenter :
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.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
class GuestControllerTest extends
TestCase {
public
function
testGetLogin()
{
$response
=
$this
->
call('GET'
,
'guest/login'
);
$this
->
assertResponseOk();
}
public
function
testGetReset()
{
$response
=
$this
->
call('GET'
,
'guest/reset/token'
);
$this
->
assertResponseOk();
}
public
function
testGetOubli()
{
$response
=
$this
->
call('GET'
,
'guest/oubli'
);
$this
->
assertResponseOk();
}
public
function
testGetInscription()
{
$response
=
$this
->
call('GET'
,
'guest/inscription'
);
$this
->
assertResponseOk();
}
public
function
testPostLoginOk()
{
$this
->
assertTrue
(Auth::
guest());
// Envoi du formulaire
$response
=
$this
->
call('POST'
,
'guest/login'
,
array
(
'username'
=>
'admin'
,
'password'
=>
'admin'
)
);
// Assertions
$this
->
assertTrue
(Auth::
check());
$this
->
assertRedirectedToRoute('accueil'
);
$this
->
assertSessionHas('flash_notice'
,
'Vous avez été correctement connecté avec le pseudo '
.
Auth::
user()->
username);
}
public
function
testPostLoginNok()
{
// Envoi du formulaire
$response
=
$this
->
call('POST'
,
'guest/login'
);
// Assertions
$this
->
assertTrue
(Auth::
guest());
$this
->
assertRedirectedTo('guest/login'
);
$this
->
assertSessionHas('flash_error'
,
'Pseudo ou mot de passe non correct !'
);
}
public
function
testPostOubliOk()
{
$user
=
User::
find(1
);
// Envoi du formulaire
$response
=
$this
->
call('POST'
,
'guest/oubli'
,
array
('email'
=>
$user
->
email));
// Assertions
$this
->
assertRedirectedTo('guest/oubli'
);
$this
->
assertSessionHas('flash_notice'
,
'Un mail vous a été envoyé, veuillez suivre ses indications pour renouveler votre mot de passe.'
);
}
public
function
testPostOubliNok()
{
// Envoi du formulaire
$response
=
$this
->
call('POST'
,
'guest/oubli'
);
$this
->
assertRedirectedTo('guest/oubli'
);
}
public
function
testPostInscriptionOk()
{
// Envoi du formulaire
$response
=
$this
->
call('POST'
,
'guest/inscription'
,
array
(
'username'
=>
'Dupontel'
,
'password'
=>
'jolipasse'
,
'email'
=>
'mail@moi.com'
,
'password_confirmation'
=>
'jolipasse'
)
);
// Assertions
$this
->
assertEquals(1
,
User::
where('username'
,
'Dupontel'
)->
count());
$this
->
assertRedirectedToRoute('accueil'
);
$this
->
assertSessionHas('flash_notice'
,
'Votre compte a été créé.'
);
}
public
function
testPostInscriptionNok()
{
// Envoi du formulaire
$response
=
$this
->
call('POST'
,
'guest/inscription'
);
// Assertion
$this
->
assertRedirectedTo('guest/inscription'
);
}
public
function
testPostResetOk()
{
$token
=
sha1('token'
);
$user
=
User::
find(1
);
// Mot de passe initial
$passe1
=
$user
->
password;
// Création du token dans la base
DB::
table('password_reminders'
)->
insert(
array
(
'email'
=>
$user
->
email,
'token'
=>
$token
,
'created_at'
=>
new
DateTime)
);
// Envoi du formulaire
$response
=
$this
->
call('POST'
,
'guest/reset/'
.
$token
,
array
(
'token'
=>
$token
,
'mail'
=>
$user
->
email,
'password'
=>
'jolipasse'
,
'password_confirmation'
=>
'jolipasse'
)
);
// Nouveau mot de passe
$user
=
User::
find(1
);
$passe2
=
$user
->
password;
// Assertions
$this
->
assertNotEquals($passe1
,
$passe2
);
$this
->
assertRedirectedToRoute('accueil'
);
}
public
function
testPostResetNok()
{
// Envoi du formulaire
$response
=
$this
->
call('POST'
,
'guest/reset/test'
);
// Assertions
$this
->
assertRedirectedTo('guest/reset/test'
);
}
}
L'action la plus délicate à tester concerne la réinitialisation du mot de passe. Il faut en effet renseigner la base avec un token (jeton) pour que tout se déroule bien. Lançons une dernière fois PHPUnit :
Remarquez l'information sur l'envoi d'un email.
On pourrait évidemment imaginer ces tests d'une façon différente. Le but de cette partie est juste de vous montrer comment écrire des tests pour Laravel 4. Ces tests sont importants, ne les négligez pas. Il existe même une démarche de développement dirigé par les tests et même de développement dirigé par le comportement. On peut même aller plus loin et générer le squelette des classes à partir des tests…