Skip to content
Caution: You are browsing the legacy symfony 1.x part of this website.

Tests unitaires du modèle

Symfony version
Language

Ecrire des tests unitaires pour votre modèle Propel ou Doctrine est simple. Dans ce tutoriel, vous allez apprendre quelques astuces intéressantes ainsi que des bonnes pratiques pour écrire de meilleurs tests pour vos modèles.

Configuration de la base de données

Pour tester une classe du modèle Propel, vous devez avoir une base de données. Vous avez déjà celle que vous utilisez pour le développement, mais c'est une bonne habitude d'en créer une autre dédiée au tests.

Comme tous les tests sont exécutés sous l'environnement test, nous avons juste besoin d'éditer le fichier de configuration config/databases.yml et redéfinir les paramètres par défaut pour l'environnement test :

test:
  propel:
    param:
      dsn:  mysql:dbname=monprojet_test;host=localhost
 
dev:
  # dev configuration
 
all:
  propel:
    class: sfPropelDatabase
    param:
      dsn:        mysql:dbname=monprojet;host=localhost
      username:   utilisateur
      password:   pa$$word
      encoding:   utf8
      persistent: true
      pooling:    true
      classname:  PropelPDO

Dans ce cas, nous avons seulement changé le nom de la base de données, mais vous pouvez aussi changer le SGBD et utiliser SQLite par exemple.

Maintenant que nous avons configuré la base de données, nous pouvons créer les tables en utilisant la tâche propel:insert-sql :

$ php symfony propel:insert-sql --env=test

Données de Test

Nous avons besoin de charger des données de test (fixtures) chaque fois que nous voulons lancer les tests unitaires. Cela est dû au fait que nous voulons remettre la base de données dans le même état à chaque série de tests.

C'est assez simple grâce à la classe sfData :

$loader = new sfPropelData();
$loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');

La méthode loadData() prend en premier argument un répertoire ou un fichier. Un dossier fixtures courant ressemble à ça :

test/
  fixtures/
    10_categories.yml
    20_articles.yml
    30_commentaires.yml

Notez les nombres préfixant tous les noms de fichier. C'est une manière simple de contrôler l'ordre de chargement des données. Plus tard dans le projet, si nous devons insérer quelques fichiers de fixtures, il sera facile d'avoir des numéros libres entre ceux existants :

test/
  fixtures/
    10_categories.yml
    15_doit_etre_charge_entre_categories_et_articles.yml
    20_articles.yml
    30_comments.yml

Les lecteurs astucieux auront remarqué que nous avons mis les fixtures dans le dossier test/, alors que le livre symfony recommende de le mettre dans data/. C'est vraiment une question de goûts, mais j'aime organiser mes fixtures dans ces deux répertoires car les fixtures peuvent être catégorisés dans deux groupes différents :

  • data/fixtures: contient les données initiales pour faire tourner l'application
  • test/fixtures: contient les données utiles aux tests (unitaires and fonctionnels)

Cette disposition simple fonctionne bien lorsque vous avez peu de données de tests, mais quand votre modèle grossira, vous aurez de plus en plus de fixtures, et le temps pris pour les charger dans la base de données deviendra significatif. Donc, nous avons besoin de pouvoir charger uniquement un sous-ensemble de nos données de test. Une façon de le faire est de sous-catégoriser vos données de test en créant un sous-répertoire par fonctionnalité principale :

test/
  fixtures/
    10_cms/
      10_categories.yml
      20_articles.yml
      30_comments.yml
    20_forum/
      10_threads.yml

A présent, au lieu de charger le répertoire principal fixtures, nous pouvons simplement charger l'un des sous-répertoires, en fonction de la classe du modèle à tester. Mais la plupart du temps, vous devez également charger des données partagées, comme les utilisateurs :

test/
  fixtures/
    00_commun/
      10_utilisateurs.yml
    10_cms/
      10_categories.yml
      20_articles.yml
      30_comments.yml
    20_forum/
      10_threads.yml

Pour simplifier ce cas d'utilisation, la méthode loadData() est capable de prendre en argument un tableau de répertoires et/ou de fichiers :

// charger les utilisateurs et toutes les données du CMS
$loader = new sfPropelData();
$loader->loadData(array(
  sfConfig::get('sf_test_dir').'/fixtures/00_commun/10_utilisateur.yml',
  sfConfig::get('sf_test_dir').'/fixtures/10_cms',
));

Cela va charger le fichier 10_utilisateur.yml puis tous les fichiers du dossier 10_cms.

Ecrire des tests unitaires

Maintenant que nous avons une base de données dédiée et un moyen de l'avoir dans un état connu, créons quelques tests unitaires pour le modèle Article.

Voici un fichier de lancement de test unitaire Propel typique :

// test/unit/model/ArticlePeerTest.php
include(dirname(__FILE__).'/../../bootstrap/unit.php');
 
$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'test', true);
 
new sfDatabaseManager($configuration);
 
$loader = new sfPropelData();
$loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');
 
$t = new lime_test(1, new lime_output_color());
 
$t->diag('::retrieveBySlug()');
$article = ArticlePeer::retrieveBySlug('le-meilleur-framework-de-tous-les-temps');
$t->is($article->getTitle(), 'Le meilleur framework de tous les temps', '->retrieveBySlug() retourne un article dont le titre correspond au slug demandé');

Le script parle de lui-même :

  • Comme pour chaque test unitaire, nous incluons le fichier de bootstrap (lancement)

    include(dirname(__FILE__).'/../../bootstrap/unit.php');
  • Nous créons un objet de configuration pour l'environnement test et nous activons le debugging :

    $configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'test', true);

    Cela va aussi initialiser le chargement automatique de toutes les classes Propel.

  • Nous créons un gestionnaire de base de données. Il initialise la connexion Propel en chargeant le fichier de configuration databases.yml :

    new sfDatabaseManager($configuration);
  • Nous chargeons nos données de test en utilisaant sfPropelData :

    $loader = new sfPropelData();
    $loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');
  • Maintenant que tout est en place, nous pouvons démarrer le test de notre objet modèle.

Si vous n'avez pas l'habitude d'écrire des tests unitaires, cela peut être intimidant au début.

Voici quelques astuces que j'utilise tout le temps pour savoir ce que j'ai besoin de tester :

  • Tester une méthode de classe à la fois
  • La tester pour une entrée donnée, la sortie doit être celle attendue
  • Lire le code de la méthode et tester toutes les règles métier que vous pouvez avoir
  • Ne jamais tester les les choses évidentes et les choses faites par une autre méthode

Mes fichiers de test se présentent toujours de la même manière :

// afficher un message avec le nom de la méthode testée (-> pour les méthodes d'instances, :: pour les méthodes statiques)
$t->diag('->NomDeLaMethode()');
 
// Tester une chose à la fois peut être exprimé en une phrase simple
// La phrase commence toujours avec le nom de la méthode
// puis un verbe pour expliquer ce qui doit être fait, quel est le comportement...
$t->is($object->NomDeLaMethode(), 1, '->NomDeLaMethode() retourne 1 si aucun argument n\'est passé');

Couverture de code

Lorsque vous écrivez des tests, il est facile d'oublier une condition dans un code complexe.

Symfony 1.2 fournit une tâche rudement pratique pour tester la couverture de code, test:coverage.

Donc, après que mes tests aient été écrits pour une classe données, je lance toujours la tâche test:coverage pour être sûr d'avoir tout testé :

$ php symfony test:coverage test/unit/model/ArticleTest.php lib/model/Article.php

Le premier argument est un fichier ou un répertoire de test. Le second est un fichier ou un répertoire pour lequel vous voulez connaître la couverture de code.

Si vous désirez savoir quelles lignes n'ont pas été couvertes, ajoutez simplement l'option --detailed :

$ php symfony test:coverage --detailed test/unit/model/ArticleTest.php lib/model/Article.php

This work is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.