• 15 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 11/05/2020

Les tests unitaires

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

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 ça 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.

L'intendance des tests

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 :

"require-dev": {
    "fzaninotto/faker": "~1.4",
    "mockery/mockery": "0.9.*",
    "phpunit/phpunit": "~4.0",
    "symfony/css-selector": "2.8.*|3.0.*",
    "symfony/dom-crawler": "2.8.*|3.0.*"
},

Mais vous pouvez aussi utiliser le fichierphar que vous pouvez trouver sur cette page et le placer à la racine de votre application et vous êtes prêt à tester !

Vous pouvez vérifier que ça fonctionne en entrant cette commande :

php phpunit.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 :).

Dans tous les exemples de ce chapitre j'utiliserai le fichierphar.

L'intendance de Laravel

Si vous regardez les dossiers de Laravel vous allez trouver un dossier qui est consacré aux tests :

Les dossier des tests
Les dossier des tests

Vous avez déjà deux fichiers. VoilàTestCase.php :

<?php

class TestCase extends Illuminate\Foundation\Testing\TestCase
{
    /**
     * The base URL to use while testing the application.
     *
     * @var string
     */
    protected $baseUrl = 'http://localhost';

    /**
     * Creates the application.
     *
     * @return \Illuminate\Foundation\Application
     */
    public function createApplication()
    {
        $app = require __DIR__.'/../bootstrap/app.php';

        $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

        return $app;
    }
}

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 classeIlluminate\Foundation\Testing\TestCase qui elle-même étend la classePHPUnit_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 classeTestCase. Il y a un exemple de test déjà présentExampleTest.php :

<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class ExampleTest extends TestCase
{
    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testBasicExample()
    {
        $this->visit('/')
             ->see('Laravel 5');
    }
}

Sans entrer pour le moment dans le code sachez simplement qu'on envoie une requête pour la route de base et qu'on attend une réponse. Pour lancer ce test c'est très simple, entrez cette commande :

php phpunit.phar
PHPUnit 4.8.21 by Sebastian Bergmann and contributors.

.

Time: 3290ms, Memory: 21.50Mb

OK (1 test, 2 assertions)

On voit qu'ont été effectués un test et 2 assertions et que tout s'est bien passé.

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 fichierphpunit.xml :

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="bootstrap/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Application Test Suite">
            <directory suffix="Test.php">./tests</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
            <exclude>
                <file>./app/Http/routes.php</file>
            </exclude>
        </whitelist>
    </filter>
    <php>
        <env name="APP_ENV" value="testing"></env>
        <env name="CACHE_DRIVER" value="array"></env>
        <env name="SESSION_DRIVER" value="array"></env>
        <env name="QUEUE_DRIVER" value="sync"></env>
    </php>
</phpunit>

On trouve déjà 4 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 afile),

  • SESSION_DRIVER : en mode "array" ce qui signifie qu'on ne va pas faire persister la session (par défaut on afile),

  • QUEUE_DRIVER : je n'évoque pas les tâches dans ce cours, on ne va donc pas tenir compte de cette variable.

On peut évidemment ajouter les variables dont on a besoin. Par exemple si pendant les tests je ne veux plusMySql maissqlite. Il y a une variable dansconfig/database.php :

<?php
'default' => env('DB_CONNECTION', 'mysql'),

Il faut donc la renseigner dans.env :

DB_CONNECTION=mysql

Du coup dansphpunit.xml je peux écrire :

<env name="DB_CONNECTION" value="sqlite"/>

Maintenant pour les tests je vais utilisersqlite.

Construire un test

Les trois étapes d'un test

Pour construire un test on procède généralement en trois étapes :

  1. on initialise les données,

  2. on agit sur ces données,

  3. 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éthodetestBasicExample par celui-ci :

<?php
public function testBasicExample()
{
	$data = [10, 20, 30];
	$result = array_sum($data);
	$this->assertEquals(60, $result);
}

On trouve nos trois étapes. On initialise les données :

<?php
$data = [10, 20, 30];

On agit sur ces données :

<?php
$result = array_sum($data);

On teste le résultat :

<?php
$this->assertEquals(60, $result);

La méthodeassertEquals permet de comparer deux valeurs, ici 60 et$result. Si vous lancez le test vous obtenez :

OK (1 test, 1 assertion)

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 :

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

Vous connaissez maintenant le principe de base d'un test et ce qu'on peut obtenir comme renseignement en cas d'échec.

Assertions et appel de routes

Les assertions

Les assertions constituent l'outil de base des tests. On en a vu une ci-dessus et il en existe bien d'autres. Vous pouvez en trouver la liste complète ici.

Voici l'utilisation de quelques assertions et l'utilisation d'un helper de Laravel que l'on teste au passage :

<?php
public function testBasicExample()
{
    $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 :

OK (1 test, 5 assertions)

Un test et 5 assertions correctes.

Appel de routes 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 :

<?php
Route::get('/', function()
{
    return 'coucou';
});

On a donc une requête avec l'url de base et comme réponse la chaîne "coucou". Nous allons tester que la requête aboutit bien, qu'il y a une réponse correcte et que la réponse est "coucou" :

<?php
public function testBasicExample()
{
    $response = $this->call('GET', '/');
	$this->assertResponseOk();
	$this->assertEquals('coucou', $response->getContent());
}

L'assertionassertResponseOk nous assure que la réponse est correcte. Ce n'est pas une assertion de PHPUnit mais une spécifique de Laravel. La méthodegetContent permet de lire la réponse. On obtient :

OK (1 test, 2 assertions)

Les vues et les contrôleurs

Les vues

Qu'en est-il si on retourne une vue ? Mettez ce code pour la route :

<?php
Route::get('/', function()
{
    return view('welcome')->with('message', 'Vous y êtes !');
});

Ajoutez dans cette vue ceci :

{{ $message }}

Maintenant voici le test :

<?php
public function testBasicExample()
{
    $response = $this->call('GET', '/');
	$view = $response->original;
	$this->assertEquals('Vous y êtes !', $view['message']);
}

On envoie la requête et on récupère la réponse. Ensuite la méthodeoriginal nous permet de récupérer la vue. On peut alors tester la valeur de la variable$message dans la vue.

Laravel propose une autre assertion pour effectuer la même chose :

<?php
public function testBasicExample()
{
  $response = $this->call('GET', '/');
  $this->assertViewHas('message');
  $this->assertViewHas('message', 'Vous y êtes !');
}

L'assertionassertViewHas permet de vérifier qu'une vue reçoit bien une donnée, on peut aussi grâce au second paramètre vérifier sa valeur.

Les contrôleurs

Créez ce contrôleur :

<?php

namespace App\Http\Controllers;

class WelcomeController extends Controller
{
	public function index()
	{
		return view('welcome');
	}
}

Vous pouvez tester facilement les actions d'un contrôleur. Créez cette route pour mettre en oeuvre le contrôleur ci-dessus :

<?php
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 fichierExampleTest.php qui ne va plus nous servir.

Créez cette architecture de dossiers et le fichierWelcomeControllerTest.php :

Le dossier des tests

Et mettez ce code dans le fichierWelcomeControllerTest.php :

<?php

class WelcomeControllerTest extends TestCase
{

    public function testIndex()
	{
		$response = $this->action('GET', 'WelcomeController@index');
		$this->assertResponseOk();
	}

}

Si vous lancez le test ça devrait bien se passer :

OK (1 test, 1 assertion)

Donc pour appeler une action d'un contrôleur il suffit d'utiliser la méthodeaction et de préciser le verbe, le nom du contrôleur et le nom de la méthode associée. D'autre part il est important de bien organiser les tests pour s'y retrouver. Une bonne façon de procéder est d'adopter la même architecture de dossiers que  celle prévue pour l'application.

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.

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 :

"require-dev": {
    ...
	"mockery/mockery": "0.9.*"
},

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 :

Le composant Mockery

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ôleurWelcomeController par celui-ci :

<?php

namespace App\Http\Controllers;

use Livre;

class WelcomeController extends Controller
{

	public function __construct()
	{
		$this->middleware('guest');
	}

	public function index(Livre $livre)
	{
		$livres = $livre->all();
		return view('welcome', compact('livres'));
	}

}

J'ai prévu l'injection d'un modèle dans la fonctionindex. Ce n'est pas forcément très judicieux d'un point de vue conceptuel et il vaudrait mieux passer par un repository comme nous l'avons vu plusieurs fois dans ce cours mais on va prendre cette situation simple pour exécuter un test. D'ailleurs la classeLivre n'a pas besoin de vraiment exister puisqu'on ne va pas vraiment l'utiliser.

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 :

<?php

use Illuminate\Database\Eloquent\Collection;

class WelcomeControllerTest extends TestCase
{

    public function testIndex()
	{
		// Création de la collection
		$collection = new Collection;
		$i = 2;
		while($i--) {
			$collection->add((object) ['titre' => 'Titre' . $i]);
		}

		// Création Mock
		$mock = Mockery::mock('Livre');
		$mock ->shouldReceive('all')
			  ->once()
			  ->andReturn($collection);

		// Création lien
		$this->app->instance('Livre', $mock);

		// Action
		$response = $this->call('GET', 'welcome');

		// Assertions
		$this->assertResponseOk();
		$this->assertViewHas('livres');

	}

	public function tearDown()
	{
		Mockery::close();
	}

}

Et voici le code à ajouter dans la vue pour faire réaliste :

@foreach ($livres as $livre)
    <p>Titre : {{ $livre->titre }}</p>
@endforeach

Si je lance le test j'obtiens :

OK (1 test, 2 assertions)

Un test et deux assertions correctes. Voyons de plus près ce code.

Au départ on crée une collection de livres :

<?php
// Création de la collection
$collection = new Collection;
$i = 2;
while($i--) {
    $collection->add((object) ['titre' => 'Titre' . $i]);
}

On crée ensuite un objet Mock en lui demandant de simuler la classeLivre  :

<?php
$mock = Mockery::mock('Livre');

Ensuite on définit le comportement que l'on désire pour cet objet :

<?php
$mock ->shouldReceive('all')
      ->once()
	  ->andReturn($collection);

On lui dit que s'il reçoit l'appel de la méthode "all" (shouldReceive) une fois (once) il doit retourner la collection $collection (andReturn). 

Ensuite on informe le conteneur de Laravel de la liaison entre la classe Livre‌ et notre objet Mock :

<?php
$this->app->instance('Livre', $mock);

C'est une façon de dire à Laravel : chaque fois que tu auras besoin de la classeLivre tu iras plutôt utiliser l'objet Mock.

Ensuite on fait l'action, ici la requête :

<?php
$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 les livres dans la vue :

<?php
$this->assertResponseOk();
$this->assertViewHas('livres');

Pour finir on prévoit pour travailler proprement de supprimer l'objet Mock à l'issue du test dans la méthodetearDown qui sert justement à effecteur des actions après un test :

<?php
public function tearDown()
{
    Mockery::close();
}

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, quant à 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.

Tester une application

Laravel va encore plus loin dans la convivialité pour les tests en offrant la possibilité de tester facilement une application. 

Partez d'un Laravel tout neuf et ajoutez l'authentification avec la commande :

php artisan make:auth

Prévoyez une base de données pour que ça fonctionne et entrez l'utilisateur Toto, avec l'email "toto@chez.fr" et le mot de passe "password". On va maintenant créer un test pour voir si cet utilisateur peut bien s'authentifier.

Commencez par supprimer le fichierExampleTest.php.

Créez un nouveau fichierAuthControllerTest.php avec Artisan :

php artisan make:test AuthControllerTest

Complétez ainsi le code :

<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class AuthControllerTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testRegister()
    {
        $this->visit('/')
        	 ->click('Login')
             ->type('toto@chez.fr', 'email')
             ->type('password', 'password')
             ->press('Login')
             ->see('Dashboard');
    }
}

Vous voyez que ça se lit comme de la prose ! Si vous lancez le test vous obtiendrez :

OK(2 tests, 5 assertions)

Je ne vais pas détailler ici toutes les possibilités, reportez vous à la documentation.

En résumé

  • Laravel utilise PHPUnit pour effectuer les test 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.

Exemple de certificat de réussite
Exemple de certificat de réussite