VII. Les tests▲
Les développeurs PHP n'ont pas été habitués à faire des tests pour leurs applications. Cela est dû à l'histoire de ce langage qui n'était au départ qu'une possibilité de scripter au milieu du code HTML, mais qui s'est peu à peu développé comme un langage de plus en plus évolué. Les créateurs de frameworks ont initié une autre façon d'organiser le code de PHP : en particulier ils ont mis en avant la séparation des tâches qui a rendu la création de tests possible.
Laravel a été pensé pour intégrer des tests. Il comporte une infrastructure élémentaire et des helpers. Nous allons voir dans ce chapitre cet aspect de Laravel. Considérez ce que je vais vous dire ici comme une simple introduction à ce domaine qui mériterait à lui seul un cours spécifique. Je vais m'efforcer de vous démontrer l'utilité de créer des tests, comment les préparer et comment les isoler.
Lorsqu'on développe avec PHP, on effectue forcément des tests, au moins manuels. Par exemple, si vous créez un formulaire, vous allez l'utiliser, entrer diverses informations, essayer des fausses manœuvres… Imaginez que tout cela soit automatisé et que vous n'ayez qu'à cliquer pour lancer tous les tests. C'est le propos de ce chapitre.
Vous pouvez aussi vous dire qu'écrire des tests conduit à du travail supplémentaire, que ce n'est pas toujours facile, que ce n'est pas nécessaire dans tous les cas. C'est à vous de voir si vous avez besoin d'en créer ou pas. Pour des petites applications la question reste ouverte. Par contre, dès qu'une application prend de l'ampleur ou lorsqu'elle est conduite par plusieurs personnes, alors il devient vite nécessaire de créer des tests automatisés.
VII-A. L'intendance des tests▲
VII-A-1. PHPUnit▲
Laravel utilise PHPUnit pour effectuer les tests. C'est un framework créé par « Sebastian Bergmann » qui fonctionne à partir d'assertions.
Ce framework est installé comme dépendance de Laravel en mode développement :
2.
3.
4.
5.
6.
"require-dev"
:
{
"filp/whoops"
:
"~2.0"
,
"fzaninotto/faker"
:
"~1.4"
,
"mockery/mockery"
:
"0.9.*"
,
"phpunit/phpunit"
:
"~6.0"
},
Mais vous pouvez aussi utiliser le fichier phar, que vous pouvez trouver sur cette page, et le placer à la racine de votre application. Vous êtes alors prêt à tester !
La version 6 de PHPUnit nécessite au minimum PHP 7 |
Vous pouvez vérifier que ça fonctionne en entrant cette commande :
php phpunit
(
numéro de version).phar -h
Vous obtenez ainsi la liste de toutes les commandes disponibles.
Si vous utilisez la version installée avec Laravel ça donne :
php vendor\phpunit\phpunit\phpunit -h
Je vous conseille de vous faire un alias !
Il y a aussi la possibilité d'utiliser les fichiers placés dans le dossier vendor/bin.
Dans tous les exemples de ce chapitre j'utiliserai le fichier phar en réduisant son nom à phpunit.
VII-B. L'intendance de Laravel▲
Si vous regardez les dossiers de Laravel vous allez en trouver un qui est consacré aux tests :
Vous avez déjà deux dossiers et deux fichiers. Voilà le code de TestCase.php :
2.
3.
4.
5.
6.
7.
8.
9.
10.
<?php
namespace
Tests;
use
Illuminate\Foundation\Testing\TestCase as
BaseTestCase;
abstract
class
TestCase extends
BaseTestCase
{
use
CreatesApplication;
}
On voit qu'on utilise le trait CreatesApplication.
Cette classe est chargée de créer une application pour les tests dans un environnement spécifique (ce qui permet de mettre en place une configuration adaptée aux tests).
Elle étend la classe Illuminate\Foundation\Testing\TestCase, qui elle-même étend la classe PHPUnit\Framework\TestCase, en lui ajoutant quelques fonctionnalités bien pratiques, comme nous allons le voir.
Toutes les classes de test que vous allez créer devront étendre cette classe TestCase. |
On a deux exemples de tests déjà présents, un dans Unit\ExampleTest.php :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
<?php
namespace
Tests\Unit;
use
Tests\TestCase;
use
Illuminate\Foundation\Testing\RefreshDatabase;
class
ExampleTest extends
TestCase
{
/**
* A basic test example.
*
*
@return
void
*/
public
function
testBasicTest()
{
$this
->
assertTrue
(true
);
}
}
et un autre dans Features\ExampleTest.php :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
<?php
namespace
Tests\Feature;
use
Tests\TestCase;
use
Illuminate\Foundation\Testing\RefreshDatabase;
class
ExampleTest extends
TestCase
{
/**
* A basic test example.
*
*
@return
void
*/
public
function
testBasicTest()
{
$response
=
$this
->
get('/'
);
$response
->
assertStatus(200
);
}
}
Pourquoi deux dossiers ? |
Si on lit la documentation sur le sujet on trouve cette explication :
By default, your application's tests directory contains two directories: Feature and Unit. Unit tests are tests that focus on a very small, isolated portion of your code. In fact, most unit tests probably focus on a single method. Feature tests may test a larger portion of your code, including how several objects interact with each other or even a full HTTP request to a JSON endpoint.
En gros, dans Feature, on va mettre des tests plus généraux, pas vraiment unitaires pour le coup. Mais vous pouvez utiliser ces deux dossiers « à votre convenance », et donc n'en utiliser qu'un seul.
Sans entrer pour le moment dans le code, sachez simplement que dans le premier exemple, on se contente de demander si un truc vrai est effectivement vrai (bon c'est sûr que ça devrait être vrai ^^). Dans le second, on envoie une requête pour la route de base et on attend une réponse positive (200).
Pour lancer ces tests, c'est très simple : entrez cette commande :
On voit qu'ont été effectués deux tests et deux assertions, et que tout s'est bien passé.
VII-B-1. L'environnement de test▲
Je vous ai dit que les tests s'effectuent dans un environnement particulier, ce qui est bien pratique.
Où se trouve cette configuration ? |
Regardez le fichier phpunit.xml :
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.
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
backupGlobals
=
"false"
backupStaticAttributes
=
"false"
bootstrap
=
"vendor/autoload.php"
colors
=
"true"
convertErrorsToExceptions
=
"true"
convertNoticesToExceptions
=
"true"
convertWarningsToExceptions
=
"true"
processIsolation
=
"false"
stopOnFailure
=
"false"
>
<testsuites>
<testsuite
name
=
"Feature"
>
<directory
suffix
=
"Test.php"
>
./tests/Feature</directory>
</testsuite>
<testsuite
name
=
"Unit"
>
<directory
suffix
=
"Test.php"
>
./tests/Unit</directory>
</testsuite>
</testsuites>
<filter>
<whitelist
processUncoveredFilesFromWhitelist
=
"true"
>
<directory
suffix
=
".php"
>
./app</directory>
</whitelist>
</filter>
<php>
<env
name
=
"APP_ENV"
value
=
"testing"
/>
<env
name
=
"CACHE_DRIVER"
value
=
"array"
/>
<env
name
=
"SESSION_DRIVER"
value
=
"array"
/>
<env
name
=
"QUEUE_DRIVER"
value
=
"sync"
/>
</php>
</phpunit>
On trouve déjà quatre variables d'environnement :
- APP_ENV : là, on dit qu'on est en mode testing ;
- CACHE_DRIVER : en mode array, ce qui signifie qu'on ne va rien mettre en cache pendant les tests (par défaut on a file) ;
- SESSION_DRIVER : en mode array, ce qui signifie qu'on ne va pas faire persister la session (par défaut on a file) ;
- QUEUE_DRIVER : en mode sync, donc on aura pas de file d'attente.
On peut évidemment ajouter les variables dont on a besoin. Par exemple si pendant les tests je ne veux plus MySql mais sqlite : il y a une variable dans le fichier .env :
DB_CONNECTION=mysql
que je peux modifier. Pour ce faire, dans phpunit.xml, je peux écrire :
<env
name
=
"DB_CONNECTION"
value
=
"sqlite"
/>
Maintenant, pour les tests, je vais utiliser sqlite.
VII-C. Construire un test▲
VII-C-1. Les trois étapes d'un test▲
Pour construire un test, on procède généralement en trois étapes :
- On initialise les données ;
- On agit sur ces données ;
- On vérifie que le résultat est conforme à notre attente.
Comme tout ça est un peu abstrait, prenons un exemple. Remplacez le code de la méthode testBasicTest (peu importe dans quel dossier) par celui-ci :
Supprimez le test dans l'autre dossier pour éviter de polluer les résultats. |
On trouve nos trois étapes. On initialise les données :
$data
=
[
10
,
20
,
30
];
On agit sur ces données :
$result
=
array_sum($data
);
On teste le résultat :
$this
->
assertEquals(60
,
$result
);
La méthode assertEquals permet de comparer deux valeurs, ici 60 et $result
. Si vous lancez le test vous obtenez :
Vous voyez à nouveau l'exécution d'un test et d'une assertion. Le tout s'est bien passé. Changez la valeur 60 par une autre et vous obtiendrez ceci :
Vous connaissez maintenant le principe de base d'un test et ce qu'on peut obtenir comme renseignement en cas d'échec.
VII-D. Assertions et appel de route▲
VII-D-1. Les assertions▲
Les assertions constituent l'outil de base des tests.
Voici quelques assertions et l'utilisation d'un helper de Laravel que l'on teste au passage :
2.
3.
4.
5.
6.
7.
8.
9.
public function testBasicTest()
{
$data
=
'
Je suis petit
'
;
$this
->
assertTrue
(starts_with($data
,
'
Je
'
));
$this
->
assertFalse
(starts_with($data
,
'
Tu
'
));
$this
->
assertSame(starts_with($data
,
'
Tu
'
),
false);
$this
->
assertStringStartsWith('
Je
'
,
$data
);
$this
->
assertStringEndsWith('
petit
'
,
$data
);
}
Lorsqu'on lance le test, on obtient ici :
un test et cinq assertions correctes.
VII-D-2. Appel de route et test de réponse▲
Il est facile d'appeler une route pour effectuer un test sur la réponse. Modifiez la route de base pour celle-ci :
On a donc une requête avec l'URL de base et comme réponse la chaîne coucou. Nous allons vérifier que la requête aboutit bien, qu'il y a une réponse correcte, et que la réponse est coucou :
L'assertion assertSuccessful nous assure que la réponse est correcte. Ce n'est pas une assertion de PHPUnit : elle est spécifique à Laravel. Vous trouverez toutes les assertions de Laravel ici.
La méthode getContent permet de lire la réponse. On obtient :
VII-E. Les vues et les contrôleurs▲
VII-E-1. Les vues▲
Qu'en est-il si on retourne une vue ? |
Mettez ce code pour la route :
Route::
get('
/
'
,
function () {
return view('
welcome
'
)->
with('
message
'
,
'
Vous y êtes !
'
);
}
);
Ajoutez dans cette vue ceci :
{{
$message
}}
Maintenant voici le test :
On envoie la requête et on récupère la réponse. On peut tester la valeur de la variable $message
dans la vue avec l'assertion assertViewHas :
VII-E-2. Les contrôleurs▲
Créez ce contrôleur :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
<?php
namespace
App\Http\Controllers;
use
Illuminate\Http\Request;
class
WelcomeController extends
Controller
{
public
function
index()
{
return
view('welcome'
);
}
}
Créez cette route pour mettre en œuvre le contrôleur ci-dessus :
Route::
get('
welcome
'
,
'
WelcomeController@index
'
);
Vérifiez que ça fonctionne (vous aurez peut-être besoin de retoucher la vue où nous avons introduit une variable).
Supprimez le fichier ExampleTest.php qui ne va plus nous servir.
Créez ce test avec Artisan :
php artisan make:test WelcomeControllerTest
Par défaut vous obtenez ce code :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
<?php
namespace
Tests\Feature;
use
Tests\TestCase;
use
Illuminate\Foundation\Testing\RefreshDatabase;
class
WelcomeControllerTest extends
TestCase
{
/**
* A basic test example.
*
*
@return
void
*/
public
function
testExample()
{
$this
->
assertTrue
(true
);
}
}
Changez ainsi le code de la méthode :
On n'a plus de méthode spécifique pour les contrôleurs comme c'était le cas avant.
VII-F. Isoler les tests▲
Nous allons maintenant aborder un aspect important des tests qui ne s'appellent pas unitaires pour rien.
Pour faire des tests efficaces, il faut bien les isoler, donc savoir ce qu'on teste, ne tester qu'une chose à la fois et ne pas mélanger les choses. |
Ceci est possible si le code est bien organisé, ce que je me suis efforcé de vous montrer depuis le début de ce cours.
Avec PHPUnit, chaque test est effectué dans une application spécifique : il n'est donc pas possible de les rendre dépendants les uns des autres. |
En général on utilise Mockery, un composant qui permet de simuler le comportement d'une classe. Il est déjà prévu dans l'installation de Laravel en mode développement :
2.
3.
4.
5.
6.
"require-dev"
:
{
"filp/whoops"
:
"~2.0"
,
"fzaninotto/faker"
:
"~1.4"
,
"mockery/mockery"
:
"0.9.*"
,
"phpunit/phpunit"
:
"~6.0"
},
Le fait de prévoir ce composant uniquement pour le développement simplifie ensuite la mise en œuvre pour le déploiement. Normalement, vous devriez trouver ce composant dans vos dossiers :
VII-F-1. Simuler une classe▲
Nous allons voir maintenant comment l'utiliser, mais pour cela on va mettre en place le code à tester. Ce ne sera pas trop réaliste, mais c'est juste pour comprendre le mécanisme de fonctionnement de Mockery. Remplacez le code du contrôleur WelcomeController par celui-ci :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
<?php
namespace
App\Http\Controllers;
use
App\Services\Livre;
class
WelcomeController extends
Controller
{
public
function
__construct
()
{
$this
->
middleware('guest'
);
}
public
function
index(Livre $livre
)
{
$titre
=
$livre
->
getTitle();
return
view('welcome'
,
compact('titre'
));
}
}
J'ai prévu l'injection d'une classe dans la méthode index. Voilà la classe en question :
2.
3.
4.
5.
6.
7.
8.
9.
10.
<?php
namespace
App\Services;
class
Livre
{
public
function
getTitle() {
return
'Titre'
;
}
}
Bon, d'accord, ce n'est pas très joli, mais c'est juste pour la démonstration.
La difficulté ici réside dans la présence de l'injection d'une classe. Comme on veut isoler les tests, l'idéal serait de pouvoir simuler cette classe. C'est justement ce que permet de faire Mockery.
Voici la classe de test que nous allons utiliser :
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.
<?php
namespace
Tests\Feature;
use
Tests\TestCase;
use
Illuminate\Foundation\Testing\RefreshDatabase;
use
Illuminate\Support\Collection;
use
App\Services\Livre;
use
Mockery;
class
WelcomeControllerTest extends
TestCase
{
public
function
testIndex()
{
// Création Mock
$mock
=
Mockery::
mock(Livre::
class
)
->
shouldReceive('getTitle'
)
->
andReturn('Titre'
);
// Création lien
$this
->
app->
instance('\App\Services\Livre'
,
$mock
);
// Action
$response
=
$this
->
call('GET'
,
'welcome'
);
// Assertions
$response
->
assertSuccessful();
$response
->
assertViewHas('titre'
,
'Titre'
);
}
public
function
tearDown
()
{
Mockery::
close();
}
}
Et voici le code à ajouter dans la vue pour faire réaliste :
{{
$titre
}}
Si je lance le test, j'obtiens :
Voyons de plus près ce code… On crée un objet Mock en lui demandant de simuler la classe Livre :
$mock
=
Mockery::
mock(Livre::
class)
Ensuite on définit le comportement que l'on désire pour cet objet :
->
shouldReceive('
getTitle
'
)
->
andReturn('
Titre
'
);
On lui dit qu'il reçoit (shouldReceive) l'appel de la méthode getTitle et doit retourner Titre.
Ensuite on informe le conteneur de Laravel de la liaison entre la classe Livre et notre objet Mock :
$this
->
app->
instance('
\App\Services\Livre
'
,
$mock
);
C'est une façon de dire à Laravel : chaque fois que tu auras besoin de la classe Livre tu iras plutôt utiliser l'objet |
Ensuite on effectue l'action, ici la requête :
$response
=
$this
->
call('
GET
'
,
'
welcome
'
);
Pour finir, on prévoit deux assertions : une pour vérifier qu'on a une réponse correcte et la seconde pour vérifier qu'on a bien le titre dans la vue :
$response
->
assertSuccessful();
$response
->
assertViewHas('
titre
'
,
'
Titre
'
);
Vous connaissez maintenant le principe de l'utilisation de Mockery. Il existe de vastes possibilités avec ce composant.
Il n'y a pas vraiment de règle quant à la constitution des tests, ni pour ce qu'il faut tester ou pas. L'important est de comprendre comment les faire et de juger ce qui est utile ou pas selon les circonstances. Une façon efficace d'apprendre à réaliser des tests, tout en comprenant mieux Laravel, est de regarder comment ces tests ont été conçus. |
VII-G. Tester une application (dusk)▲
Laravel va encore plus loin dans la convivialité pour les tests en offrant la possibilité de tester facilement une application. « Laravel Dusk » utilise par défaut ChromeDriver, mais c'est totalement transparent.
VII-G-1. Installation▲
Il faut commencer par l'installer :
composer require --dev laravel/dusk
et ensuite utiliser cette commande :
php artisan dusk:install
Vous allez vous retrouver avec un nouveau dossier dans les tests :
Enfin il faut renseigner correctement la variable APP_URL dans le fichier .env :
APP_URL=http://monsite.dev
Pour lancer les tests, c'est :
php artisan dusk
mais on peut aussi se limiter à un groupe de tests :
php artisan dusk --group
=
groupe1
VII-G-2. Exemples▲
On va voir ça en œuvre avec l'application d'exemple. Voici l'ensemble des fichiers de test :
Ils sont classés par catégories.
Pour que ça fonctionne il faut régénérer la base de données avec les valeurs par défaut :
php artisan migrate:fresh --seed
Il faut aussi régler la locale sur en :
'
locale
'
=>
'
en
'
,
On va voir les tests du groupe login (LoginTest.php) :
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.
<?php
namespace
Tests\Browser;
use
Tests\DuskTestCase;
use
Laravel\Dusk\Browser;
class
LoginTest extends
DuskTestCase
{
/**
* Test login by name and logout
* @group login
*
*
@return
void
*/
public
function
testLoginByNameAndLogout()
{
$this
->
browse(function
(Browser $browser
) {
$browser
->
visit('/login'
)
->
type('log'
,
'Slacker'
)
->
type('password'
,
'slacker'
)
->
press('Login'
)
->
assertPathIs('/'
)
->
assertSee('Logout'
)
->
clickLink('Logout'
)
->
assertSee('Login'
);
}
);
}
/**
* Test login by email
* @group login
*
*
@return
void
*/
public
function
testLoginByEmail()
{
$this
->
browse(function
(Browser $browser
) {
$browser
->
visit('/login'
)
->
type('log'
,
'redac@la.fr'
)
->
type('password'
,
'redac'
)
->
press('Login'
)
->
assertPathIs('/'
)
->
assertSee('Logout'
)
->
clickLink('Logout'
);
}
);
}
/**
* Test login fail
* @group login
*
*
@return
void
*/
public
function
testLoginFail()
{
$this
->
browse(function
(Browser $browser
) {
$browser
->
visit('/login'
)
->
type('log'
,
'toto@la.fr'
)
->
type('password'
,
'toto'
)
->
press('Login'
)
->
assertSee('These credentials do not match our records'
);
}
);
}
}
On les lance ainsi :
php artisan dusk --group
=
login
et si tout va bien :
Voyons un peu le code, par exemple testLoginByNameAndLogout. On commence par visiter la page de login :
$browser
->
visit('
/login
'
)
On entre (type) la valeur Slacker dans le champ log :
->
type('
log
'
,
'
Slacker
'
)
On entre (type) la valeur slacker dans le champ password :
->
type('
password
'
,
'
slacker
'
)
On actionne (press) le bouton Login :
->
press('
Login
'
)
On vérifie que l'URL (assertPathIs) est « / » :
->
assertPathIs('
/
'
)
On vérifie qu'il y a dans la page (assertSee) Logout :
->
assertSee('
Logout
'
)
On clique sur le lien (clickLink) Logout :
->
clickLink('
Logout
'
)
On vérifie qu'il y a dans la page (assertSee) Login :
->
assertSee('
Login
'
);
Vous voyez que c'est d'une grande simplicité ! La documentation complète est ici.
Vous pouvez explorer le code des autres fichiers et lancer les tests. Récemment il y a eu quelques bugs après en avoir lancé plusieurs. Il vaut donc mieux se limiter à un groupe.
VII-H. En résumé▲
- Laravel utilise PHPUnit pour effectuer les tests unitaires.
- En plus des méthodes de PHPUnit, on dispose d'helpers pour intégrer les tests dans une application réalisée avec Laravel.
- Le composant Mockery permet de simuler le comportement d'une classe et donc de bien isoler les tests.
- Laravel propose des commandes conviviales pour tester une application avec dusk.