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'applicationtest/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.