IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Tutoriel pour apprendre à utiliser le framework Laravel 4


précédentsommairesuivant

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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
return array(
 
    'default' => 'sqlite',
 
    'connections' => array(
        'sqlite' => array(
            'driver'   => 'sqlite',
            'database' => ':memory:',
            'prefix'   => ''
        ),
    )
);

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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 Image non disponible.

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 :

Image non disponible

Nous aurons donc la même architecture pour les tests :

Image non disponible

Et voici le contenu initial pour ces fichiers :

 
Sélectionnez
class HomeControllerTest extends TestCase {}
 
Sélectionnez
class AuthControllerTest extends TestCase {}
 
Sélectionnez
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
public function accueil()
{
    $articles = Article::select('id', 'title', 'intro_text')
                        ->orderBy('created_at', 'desc')
                        ->paginate(4);
    return $this->gen_accueil($articles);
}

Cette action est activée par cette route :

 
Sélectionnez
Route::get('/', array('uses' => 'HomeController@accueil', 'as' => 'accueil'));

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 :

 
Sélectionnez
1.
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 :

Image non disponible

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 :

 
Sélectionnez
echo var_dump($response->getContent());
Image non disponible

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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
public function testAccueil()
{
    $response = $this->call('GET', '/'); 
    $this->assertViewHas(array('categories', 'articles', 'actif'));
    $this->assertResponseOk();
}
Image non disponible

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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
public function testAccueil()
{
    $crawler = $this->client->request('GET', '/');
    $this->assertTrue($this->client->getResponse()->isOk());
    $this->assertCount(1, $crawler->filter('h1:contains("Mon joli blog !")'));
}

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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
public function testFindOk()
{
    $article = Article::find(1);
    $find = $article->intro_text;
    // Envoi du formulaire 
    $response = $this->call('POST', 'find', array('find' => $find));
    // Assertion
    $this->assertContains('<p>'.$find.'</p>', $response->getContent());
}

Voyons un peu où nous en sommes :

Image non disponible

Voyons à présent le cas du champ vide :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
public function testFindNok()
{
    // Envoi du formulaire 
    $response = $this->call('POST', 'find');
    // Assertions
    $this->assertRedirectedTo('/'); 
    $this->assertSessionHas('flash_error', 'Il faudrait entrer un terme pour la recherche !');       
}

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 :

 
Sélectionnez
1.
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 » :

 
Sélectionnez
1.
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 :

Image non disponible

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 :

 
Sélectionnez
1.
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 :

Image non disponible

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…


précédentsommairesuivant

Copyright © 2017 Laravel. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.