Au cours des deux derniers jours, nous avons examiné tous les éléments appris au cours des cinq premiers jours du calendrier de l'avent, afin de personnaliser les fonctionnalités de Jobeet et d'en ajouter de nouvelles. Dans le processus, nous avons également abordé d'autres fonctionnalités plus avancées de symfony.
Aujourd'hui, nous allons commencer à parler de quelque chose de complètement différent : les tests automatisés. Comme le sujet est assez vaste, il nous faudra deux jours complets pour tout couvrir.
Les tests dans symfony
Il existe deux différents types de tests automatisés dans symfony: les tests unitaires et les tests fonctionnels.
Les tests unitaires vérifient que chaque méthode et chaque fonction fonctionne correctement. Chaque test doit être aussi indépendante que possible des autres.
D'autre part, des tests fonctionnels vérifient que l'application résultante se comporte correctement dans son ensemble.
Tous les tests dans symfony sont situés sous le répertoire test/
du projet.
Il contient deux sous-répertoires, un pour les tests unitaires (test/unit/
) et un pour
les tests fonctionnels (test/functional/
).
Les tests unitaires seront couverts dans le tutoriel d'aujourd'hui et demain sera consacrée aux tests fonctionnels.
Les tests unitaires
L'écriture des tests unitaires est peut-être l'une des pratiques les plus difficiles du développement web à mettre en place. Car les développeurs web ne sont pas vraiment utilisé pour tester leur travail, beaucoup de questions se posent : Dois-je écrire des tests avant d'implémenter une fonctionnalité ? Que dois-je tester ? Mes tests doivent couvrir chaque cas limite ? Comment puis-je être sûr que tout est bien testé ? Mais d'habitude, la première question est beaucoup plus fondamentale : où commencer?
Même si nous insistons fortement sur les tests, l'approche de symfony est pragmatique : il est toujours préférable d'avoir quelques tests que pas de test du tout. Avez-vous déjà beaucoup de code sans aucun test ? Pas de problème. Vous n'avez pas besoin d'avoir une suite de tests complète pour bénéficier des avantages des tests. Commencez par l'ajout de test lorsque vous trouvez un bogue dans votre code. Au fil du temps, votre code va devenir de meilleure qualité, la couverture du code va augmenter, et vous deviendrez plus confiant sur ce sujet. En commençant avec une approche pragmatique, vous vous sentirez plus à l'aise avec les tests dans le temps. L'étape suivante consiste à écrire des tests pour des nouvelles fonctionnalités. En peu temps, vous allez devenir accro aux tests.
Le problème avec la plupart des bibliothèques de test est leur difficulté d'apprentissage. C'est pourquoi Symfony fournit une bibliothèque très simple de test, lime, pour faire de l'écriture de test avec une incroyable facilité.
note
Même si ce tutoriel décrit intensivement la bibliothèque intégrée lime, vous pouvez employer n'importe quelle bibliothèque de test, comme l'excellente bibliothèque PHPUnit.
Le framework de test ~lime|Framework de test Lime~
Tous les tests unitaires écrits avec le framework lime débutent avec le même code :
require_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(1, new lime_output_color());
Tout d'abord, le fichier d'amorçage unit.php
est inclus pour initialiser un certain nombre
de choses. Puis, un nouvel objet lime_test
est créé et le nombre de tests prévus à l'exécution
est passé comme un argument.
note
Le plan permet à lime d'afficher un message d'erreur au cas où trop peu de tests seraient exécutés (par exemple quand un test génère une erreur fatale PHP).
Les tests fonctionnent en appelant une méthode ou une fonction avec un ensemble d'entrées prédéfinies, puis en comparant les résultats avec ceux escomptés. Cette comparaison permet de déterminer si un test réussit ou échoue.
Pour faciliter la comparaison, l'objet lime_test
fournit plusieurs méthodes :
Méthode | Description |
---|---|
ok($test) |
Teste une condition et passe si elle est vrai |
is($value1, $value2) |
Compare deux valeurs et passe si elles sont |
égales (== ) |
|
isnt($value1, $value2) |
Compare deux valeurs et passe si elles ne sont |
pas égales | |
like($string, $regexp) |
Teste une chaîne à une expression régulière |
unlike($string, $regexp) |
Vérifie qu'une chaîne ne correspond pas à une |
expression régulière | |
is_deeply($array1, $array2) |
Vérifie que les deux tableaux ont les mêmes valeurs |
tip
Vous pouvez vous demander pourquoi lime définit tant de méthodes de test, car tous les
tests peuvent être écrits juste en employant la méthode ok()
. L'avantage du choix des
méthodes se situe dans des messages d'erreur beaucoup plus explicites en cas de test échoué
et pour une lisibilité améliorée des tests.
L'objet lime_test
fournit également d'autres méthodes de test pratique :
Méthode | Description |
---|---|
fail() |
Echoue toujours -- Utile pour tester les exceptions |
pass() |
Passe toujours -- Utile pour tester les exceptions |
skip($msg, $nb_tests) |
Compte pour $nb_tests - utile pour les tests |
conditionnels | |
todo() |
Compte pour un test - utile pour les tests qui ne |
sont pas encore écrits |
Enfin, la méthode comment($msg)
renvoie un commentaire, mais n'exécute aucun test.
Exécution des tests unitaires
Tous les tests unitaires sont stockés dans le répertoire test/unit/
. Par convention,
les tests sont nommés d'après la classe qu'ils testent et suffixé par Test
. Même si
vous pouvez organiser les fichiers sous le répertoire test/unit/
comme vous le désirez,
nous vous conseillons de reproduire la structure du répertoire lib/
.
Pour illustrer le test unitaire, nous allons tester la classe Jobeet
.
Créez un fichier test/unit/JobeetTest.php
et copiez le code suivant à l'intérieur :
// test/unit/JobeetTest.php require_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(1, new lime_output_color()); $t->pass('This test always passes.');
Pour lancer les tests, vous pouvez exécuter le fichier directement :
$ php test/unit/JobeetTest.php
Ou utilisez la tâche test:unit
:
$ php symfony test:unit Jobeet
note
Windows en ligne de commande ne peut malheureusement pas mettre en évidence les résultats des tests en couleur rouge ou verte.
Tester slugify
Commençons notre voyage dans le monde merveilleux des tests unitaires en écrivant des
tests pour la méthode Jobeet::slugify()
.
Nous avons créé la méthode ~slug|Slug~ify()
durant la journée 5 pour nettoyer une chaîne
de sorte qu'elle ne puisse pas être dangereuse dans une URL. La conversion consiste à certaines
transformations de base comme la conversion de tous les caractères non-ASCII par un tiret (-
) ou
de convertir la chaîne en minuscules:
Entrée | Sortie |
---|---|
Sensio Labs | sensio-labs |
Paris, France | paris-france |
Remplacez le contenu du fichier de test avec le code suivant:
// test/unit/JobeetTest.php require_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(6, new lime_output_color()); $t->is(Jobeet::slugify('Sensio'), 'sensio'); $t->is(Jobeet::slugify('sensio labs'), 'sensio-labs'); $t->is(Jobeet::slugify('sensio labs'), 'sensio-labs'); $t->is(Jobeet::slugify('paris,france'), 'paris-france'); $t->is(Jobeet::slugify(' sensio'), 'sensio'); $t->is(Jobeet::slugify('sensio '), 'sensio');
Si vous jetez un coup d'œil aux tests que nous avons écrit, vous remarquerez que chaque ligne teste qu'une seule chose. C'est quelque chose que vous devez garder à l'esprit lors de l'écriture des tests unitaires. Testez une chose à la fois.
Vous pouvez maintenant exécuter le fichier de test. Si tous les tests sont réussis, comme on peut s'y attendre, vous pourrez profiter de la "barre verte". Sinon, la fameuse "barre rouge" vous avertira que certains tests ne passent pas et que vous avez besoin de les corriger.
Si un test échoue, l'affichage sera de vous donner quelques informations sur la raison de cet échec, mais si vous avez des centaines de tests dans un fichier, il peut être difficile d'identifier rapidement le problème qui échoue.
Toutes les méthodes de test de lime prennent une chaîne en dernier argument qui
sert pour la description du test. C'est très pratique, car elle vous oblige à
décrire ce que font vraiment les tests. Elle peut aussi servir comme une forme de
documentation du comportement attendu d'une méthode. Ajoutons
quelques messages au fichier de test slugify
:
require_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(6, new lime_output_color()); $t->comment('::slugify()'); $t->is(Jobeet::slugify('Sensio'), 'sensio', '::slugify() converts all characters to lower case'); $t->is(Jobeet::slugify('sensio labs'), 'sensio-labs', '::slugify() replaces a white space by a -'); $t->is(Jobeet::slugify('sensio labs'), 'sensio-labs', '::slugify() replaces several white spaces by a single -'); $t->is(Jobeet::slugify(' sensio'), 'sensio', '::slugify() removes - at the beginning of a string'); $t->is(Jobeet::slugify('sensio '), 'sensio', '::slugify() removes - at the end of a string'); $t->is(Jobeet::slugify('paris,france'), 'paris-france', '::slugify() replaces non-ASCII characters by a -');
La chaîne de description du test est également un outil précieux pour comprendre ce test lors d'un essai. Vous pouvez voir une structure dans les chaînes de test : c'est des phrases décrivant comment la méthode doit se comporter et elles commencent toujours avec le nom de la méthode à tester.
Ajout de tests pour les nouvelles fonctionnalités
Le slug pour une chaîne vide est une chaîne vide. Vous pouvez le tester, il va
fonctionner. Mais une chaîne vide dans une URL, ce n'est pas une bonne idée. Modifions
donc la méthode slugify()
de sorte qu'elle retourne la chaîne "n-a" dans le cas d'une
chaîne vide.
Vous pouvez écrire le premier test, puis mettre à jour la méthode, ou l'inverse. C'est vraiment une question de goût mais l'écriture du test vous donne d'abord la confiance que votre code implémente réellement ce que vous avez prévu :
$t->is(Jobeet::slugify(''), 'n-a', '::slugify() converts the empty string to n-a');
Cette méthodologie de développement, dans laquelle vous écrivez d'abord les tests puis l'implémentation des fonctionnalités, est appelé Test Driven Development (TDD).
Si vous lancez les tests maintenant, vous devez avoir une barre rouge. Sinon, cela signifie que la fonctionnalité est déjà implémenté ou que votre test ne teste pas ce qu'il est censé tester.
Maintenant, modifiez la classe Jobeet
et ajoutez la condition suivante au début :
// lib/Jobeet.class.php static public function slugify($text) { if (empty($text)) { return 'n-a'; } // ... }
Le test doit maintenant passer comme prévu et vous pouvez profiter de la barre verte. Mais seulement si vous vous êtes rappelés de mettre à jour le plan de test. Sinon, vous aurez un message indiquant que vous avez prévu six tests et vous en avez exécuté un de plus. Avoir le nombre de tests prévus est important, car il vous tiendra au courant dès le début si le script de test échoue.
Ajout de test en raison d'un bug
Disons que le temps a passé et l'un de vos utilisateurs vous rapporte un
bogue bizarre: certains liens des emplois pointent vers une page
d'erreur 404. Après quelques recherches, vous trouvez pour une raison quelconque,
que ces emplois ont une société, une position ou un emplacement vide.
Comment cela est-il possible? Vous regardez à travers les enregistrements de la base de données et les colonnes ne
sont certainement pas vide. Vous réfléchissez pendant un moment, et hop, vous trouvez la
cause. Lorsqu'une chaîne ne contient que des caractères non-ASCII, la méthode slugify()
la
transforme en une chaîne vide. Tellement heureux d'avoir trouvé la cause, vous ouvrez la
classe Jobeet
et vous corrigez le problème immédiatement. C'est une mauvaise idée. Premièrement,
nous allons ajouter un test :
$t->is(Jobeet::slugify(' - '), 'n-a', '::slugify() converts a string that only contains non-ASCII characters to n-a');
Après avoir vérifié que le test ne passe pas, modifiez la classe Jobeet
et passez
la vérification de la chaîne vide à la fin de la méthode :
static public function slugify($text) { // ... if (empty($text)) { return 'n-a'; } return $text; }
Le nouveau test passe désormais, comme tous les autres. Le slugify()
avait
un bug en dépit de notre couverture à 100%.
Vous ne pouvez pas penser à tous les cas limites lors de l'écriture des tests et c'est très bien. Mais quand vous en découvrez un, vous devez écrire un test avant de corriger votre code. Cela signifie également que votre code va s'améliorer au fil du temps, ce qui est toujours une bonne chose.
Tests unitaires de Propel
Configuration de la base de données
Les tests unitaires d'une classe modèle Propel est un peu plus complexe, car elle requiert une connexion à la base de données. Vous avez déjà celle que vous utilisez pour votre développement, mais c'est une bonne habitude de créer une base de données dédiée pour des tests.
Durant le jour 1, nous avons introduit les environnements comme un moyen pour faire
varier les paramètres d'une application. Par défaut, tous les tests de symfony sont lancées dans
l'environnement de test
, donc nous allons configurer une base de données différentes pour l'environnement de test
:
$ php symfony configure:database --env=test "mysql:host=localhost;dbname=jobeet_test" root mYsEcret
L'option env
dit à la tâche que la configuration de la base de données est seulement pour
l'environnement de test
. Lorsque nous avons utilisé cette tâche pendant le jour 3, nous n'avions
passé aucune option env
, car la configuration a été appliquée à tous les environnements.
note
Si vous êtes curieux, ouvrez le fichier de configuration config/databases.yml
pour
voir comment symfony fait. Il est facile de modifier la configuration en fonction de
l'environnement.
Maintenant que nous avons configuré la base de données, nous pouvons l'amorcer en
employant la tâche propel:insert-sql
:
$ mysqladmin -uroot -pmYsEcret create jobeet_test $ php symfony propel:insert-sql --env=test
Données de test
Maintenant que nous avons une base de données dédiée pour nos tests, nous avons besoin d'un
moyen pour charger les données de test. Durant la journée de 3, vous avez appris à utiliser la
tâche propel:data-load
, mais pour des tests, nous avons besoin de recharger les données
chaque fois que nous les exécutons pour mettre la base de données dans un état connu.
La tâche propel:data-load
utilise en interne la
classe sfPropelData
pour charger les données :
$loader = new sfPropelData(); $loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');
note
L'objet sfConfig
peut être utilisé pour obtenir le chemin complet d'un
sous-répertoire du projet. Son utilisation permet à la structure de répertoire
par défaut pour être personnalisée.
La méthode loadData()
prend un répertoire ou un fichier comme premier argument. Elle
peut également prendre un tableau de répertoires et/ou de fichiers.
Nous avons déjà créé quelques données initiales dans le répertoire data/fixtures/
.
Pour les tests, nous mettrons les jeux de test dans le répertoire test/fixtures/
.
Ces jeux de test seront utilisés pour les tests unitaires et fonctionnels de Propel.
Pour l'instant, copiez les fichiers à partir data/fixtures/
vers le répertoire
test/fixtures/
.
Testing JobeetJob
Nous allons créer quelques tests unitaires pour la classe du modèle JobeetJob
.
Comme tous nos tests unitaires pour Propel débuteront avec le même code, créez un
fichier Propel.php
dans le répertoire de test bootstrap/
avec le code suivant :
// test/bootstrap/Propel.php include(dirname(__FILE__).'/unit.php'); $configuration = ProjectConfiguration::getApplicationConfiguration( 'frontend', 'test', true); new sfDatabaseManager($configuration); $loader = new sfPropelData(); $loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');
Le script est assez explicite :
En ce qui concerne les contrôleurs frontaux, on initialise un objet de configuration pour l'environnement
test
:$configuration = ProjectConfiguration::getApplicationConfiguration( 'frontend', 'test', true);
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 utilisant
sfPropelData
:$loader = new sfPropelData(); $loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');
note
Propel ne se connecte à la base de données que si elle a des instructions SQL à exécuter.
Maintenant que tout est en place, nous pouvons commencer à tester la classe JobeetJob
.
Premièrement, nous avons besoin de créer le fichier JobeetJobTest.php
dans test/unit/model
:
// test/unit/model/JobeetJobTest.php include(dirname(__FILE__).'/../../bootstrap/Propel.php'); $t = new lime_test(1, new lime_output_color());
Puis, nous allons commencer par ajouter un test pour la méthode getCompanySlug()
:
$t->comment('->getCompanySlug()'); $job = JobeetJobPeer::doSelectOne(new Criteria()); $t->is($job->getCompanySlug(), Jobeet::slugify($job->getCompany()), '->getCompanySlug() return the slug for the company');
Remarquez que nous ne testons que la méthode getCompanySlug()
et pas si le slug est
correct ou non, car nous l'avons déjà testé ailleurs.
L'écriture des tests pour la méthode save()
est légèrement plus complexe :
$t->comment('->save()'); $job = create_job(); $job->save(); $expiresAt = date('Y-m-d', time() + 86400 * sfConfig::get('app_active_days')); $t->is($job->getExpiresAt('Y-m-d'), $expiresAt, '->save() updates expires_at if not set'); $job = create_job(array('expires_at' => '2008-08-08')); $job->save(); $t->is($job->getExpiresAt('Y-m-d'), '2008-08-08', '->save() does not update expires_at if set'); function create_job($defaults = array()) { static $category = null; if (is_null($category)) { $category = JobeetCategoryPeer::doSelectOne(new Criteria()); } $job = new JobeetJob(); $job->fromArray(array_merge(array( 'category_id' => $category->getId(), 'company' => 'Sensio Labs', 'position' => 'Senior Tester', 'location' => 'Paris, France', 'description' => 'Testing is fun', 'how_to_apply' => 'Send e-Mail', 'email' => 'job@example.com', 'token' => rand(1111, 9999), 'is_activated' => true, ), $defaults), BasePeer::TYPE_FIELDNAME); return $job; }
note
Chaque fois que vous ajoutez des tests, n'oubliez pas de mettre à jour le nombre
de tests prévus (le plan) dans la méthode du constructeur lime_test
. Pour le fichier
JobeetJobTest
, vous devez le changer de 1 à 3.
Test sur les autres classes Propel
Vous pouvez maintenant ajouter des tests pour toutes les autres classes Propel. Comme vous êtes habitués maintenant à utiliser le processus d'écriture des tests unitaires, cela devrait être assez facile.
Validation des tests unitaires
La tâche test:unit
peut également être utilisé pour lancer tous les tests unitaires pour un projet :
$ php symfony test:unit
La tâche affiche pour chaque fichier test s'il passe ou s'il échoue :
tip
Si la tâche test:unit
retourne un "état douteux" pour un
fichier, il indique quel script échoue avant la fin. L'exécution du fichier de
test à lui seul vous donne le message d'erreur exact.
À demain
Même si les tests d'une application sont assez importants, je sais que certains d'entre vous aurez pu être tenté de sauter le tutoriel d'aujourd'hui. Je suis content que vous ne l'ayez pas fait.
Pour sûr, la compréhension de symfony est l'étude de toutes les grandes fonctionnalités du framework fournit, mais c'est aussi sa philosophie de développement et les meilleures pratiques qu'il préconise. Et le test est un d'entre eux. Tôt ou tard, les tests unitaires économiseront des jours pour vous. Ils vous donnent une confiance solide de votre code et la liberté de le refactoriser sans crainte. Les tests unitaires sont une protection qui vous alertera si vous cassez quelque chose. Le framework symfony lui-même a plus de 9000 tests.
Demain, nous allons écrire quelques tests fonctionnels pour les modules job
et
category
. Jusque-là, prenez le temps d'écrire plus de tests unitaires pour les
classes du modèle Jobeet.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.