Copyright © 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012 Sebastian Bergmann
Edition pour PHPUnit 3.6 mise à jour le 2012-10-24.
assertArrayHasKey()assertClassHasAttribute()assertClassHasStaticAttribute()assertContains()assertContainsOnly()assertCount()assertEmpty()assertEqualXMLStructure()assertEquals()assertFalse()assertFileEquals()assertFileExists()assertGreaterThan()assertGreaterThanOrEqual()assertInstanceOf()assertInternalType()assertLessThan()assertLessThanOrEqual()assertNull()assertObjectHasAttribute()assertRegExp()assertStringMatchesFormat()assertStringMatchesFormatFile()assertSame()assertSelectCount()assertSelectEquals()assertSelectRegExp()assertStringEndsWith()assertStringEqualsFile()assertStringStartsWith()assertTag()assertThat()assertTrue()assertXmlFileEqualsXmlFile()assertXmlStringEqualsXmlFile()assertXmlStringEqualsXmlString()Même les bons programmeurs font des erreurs. La différence entre un bon programmeur et un mauvais est que le bon programmeur utilise des tests pour détecter ses erreurs dès que possible. Plus vite vous faites des tests pour trouver une erreur, plus grandes sont vos chances de la trouver et moins la recherche et la correction vous coûteront cher. Ceci explique pourquoi repousser les tests juste avant la livraison d'un logiciel est tellement problématique. La plupart des erreurs ne seront pas trouvées du tout et le coût pour corriger celles qui auront été trouvées sera tel que vous devrez faire des choix car vous ne pourrez pas les corriger toutes.
Tester avec PHPUnit n'est pas une activité totalement différente de ce vous faites probablement déjà. C'est juste une façon différente de la réaliser. La différence se situe entre tester, c'est-à-dire, contrôler que votre programme se comporte de la façon attendue, et réaliser une batterie de tests, morceaux de code exécutables qui testent automatiquement la justesse de parties (unités) du logiciel. Ces morceaux de code exécutables sont appelés tests unitaires.
Dans ce chapitre, nous partirons du simple code de test à base de print
pour aboutir au test totalement automatisé. Imaginez qu'il vous a été demandé de
tester le array de PHP (tableau). Parmi les fonctionnalités à tester se
trouve la fonction count(). Pour un tableau nouvellement créé,
nous attendons que la fonction count() retourne
0. Après avoir ajouté un élément, count()
doit retourner 1.
Exemple 1.1, « Tester les opérations sur les tableaux » montre ce que
nous voulons tester.
Exemple 1.1. Tester les opérations sur les tableaux
<?php
$fixture = array();
// $fixture doit être vide.
$fixture[] = 'element';
// $fixture doit contenir un élément.
?>
Un moyen vraiment simple de contrôler que nous obtenons les résultats
que nous attendons consiste à afficher le résultat de
count() avant et après l'ajout de l'élément (voir
Exemple 1.2, « Utiliser print pour tester les opérations sur les tableaux »).
Si nous obtenons 0 puis 1,
array et count() se comportent comme
espéré.
Exemple 1.2. Utiliser print pour tester les opérations sur les tableaux
<?php
$fixture = array();
print count($fixture) . "\n";
$fixture[] = 'element';
print count($fixture) . "\n";
?>
0 1
Maintenant, nous aimerions passer de tests qui nécessitent une interprétation manuelle
à des tests qui sont exécutés automatiquement. Dans
Exemple 1.3, « Comparer les valeurs attendues et constatées pour tester les opérations sur les tableaux », nous écrivons
la comparaison entre les valeurs attendues et constatées dans le code de test et nous
affichons ok si ces valeurs sont égales. Si jamais nous voyons un message
not ok, nous savons que quelque chose ne va pas.
Exemple 1.3. Comparer les valeurs attendues et constatées pour tester les opérations sur les tableaux
<?php
$fixture = array();
print count($fixture) == 0 ? "ok\n" : "not ok\n";
$fixture[] = 'element';
print count($fixture) == 1 ? "ok\n" : "not ok\n";
?>
ok ok
Nous factorisons maintenant la comparaison des valeurs attendues et constatées dans une fonction qui lève une Exception quand il y a non correspondance (Exemple 1.4, « Utiliser une fonction d'assertion pour tester les opérations sur les tableaux »). Ceci nous offre deux avantages : l'écriture de tests devient plus facile et nous n'avons de message que quand il y a un problème.
Exemple 1.4. Utiliser une fonction d'assertion pour tester les opérations sur les tableaux
<?php
$fixture = array();
assertTrue(count($fixture) == 0);
$fixture[] = 'element';
assertTrue(count($fixture) == 1);
function assertTrue($condition)
{
if (!$condition) {
throw new Exception('Assertion en échec.');
}
}
?>
Le test est maintenant complètement automatisé. Au lieu de simplement tester comme nous le faisions dans notre première version, avec cette version, nous avons un test automatisé.
L'objectif en utilisant des tests automatisés et de faire moins d'erreurs. Tant que votre code ne sera pas parfait, même avec d'excellents tests, vous constaterez certainement une réduction drastique des défauts dès que vous commencerez à utiliser des tests automatisés. Les tests automatisés vous donnent une confiance justifiée en votre code. Vous pouvez mettre à profit cette confiance pour faire évoluer de façon plus audacieuse votre conception (refactoring), mieux collaborer avec les autres membres de votre équipe (tests communs à l'équipe), améliorer les relations avec vos clients et rentrer chez vous tous les soirs avec la preuve que le système est meilleur maintenant qu'il ne l'était ce matin grâce à vos efforts.
Arrivé à ce point, nous n'avons que deux tests pour le array de PHP
et la fonction count(). Quand nous commencerons à tester les nombreuses
fonctions array_*() que PHP offre, nous devrons écrire un test pour chacune d'elles.
Nous pourrions écrire l'infrastructure pour tous ces tests à partir de zéro. Cependant, il est bien mieux
d'écrire une infrastructure de tests une fois pour toute puis de n'écrire ensuite que les parties spécifiques
à chaque test. PHPUnit est ce type d'infrastructure.
Un framework comme PHPUnit doit résoudre une série de contraintes, certaines d'entre elles semblant toujours contradictoires avec d'autres. Les tests doivent être à la fois :
S'il est difficile à apprendre comment écrire les tests, les développeurs n'apprendront pas à les écrire.
Si les tests ne sont pas faciles à écrire, les développeurs ne les écriront pas.
Le code de test ne doit pas contenir de surcharge supplémentaire de façon à ce que le test lui-même ne soit pas perdu au milieu du bruit qui l'entoure.
Les tests doivent s'exécuter en cliquant simplement sur un bouton et afficher leurs résultats d'un façon claire et non ambiguë.
Les tests doivent s'exécuter rapidement afin qu'ils puissent être lancés des centaines ou des milliers de fois par jour.
Les tests ne doivent pas interagir les uns avec les autres. Si l'ordre dans lequel les tests sont lancés change, les résultats, eux, ne doivent pas être modifiés.
Nous devons pouvoir lancer n'importe quel nombre ou combinaison de tests ensembles. C'est un corollaire de l'indépendance.
Il existe deux antagonismes majeurs entre ces contraintes :
Les tests ne nécessitent pas généralement toute la souplesse d'un langage de programmation. De nombreux outils de test fournissent leur propre langage de script qui n'inclut que le minimum nécessaire en matière de fonctionnalité pour écrire des tests. Les tests produits sont faciles à lire et à écrire car ils ne comportent pas de bruit susceptible de vous distraire de leur contenu. Cependant, apprendre encore un autre langage de programmation et une série d'outils de programmation est malcommode et encombre l'esprit.
Si vous voulez que les résultats d'un test n'aient pas d'effets sur les résultats d'un autre, chaque test doit créer l'état complet du monde avant de commencer à s'exécuter, puis restaurer l'état initial du monde quand il a fini. Cependant, configurer le monde peut prendre un long moment : par exemple, se connecter à une base de données et l'initialiser dans un état connu en utilisant des données réalistes.
PHPUnit tente de résoudre ces conflits en utilisant PHP comme langage de test. Quelquefois, la pleine puissance de PHP est disproportionnée pour écrire de petits tests d'une ligne, mais en utilisant PHP, nous tirons profit de toute l'expérience et des outils dont disposent déjà les programmeurs. Puisque nous essayons de convaincre des testeurs réticents, limiter les obstacles pour écrire ces premiers tests est particulièrement important.
PHPUnit privilégie plutôt l'indépendance à la rapidité d'exécution. Les tests indépendants sont précieux car ils fournissent des retours de grande qualité. Vous n'obtenez pas un rapport avec un tas d'erreurs de test qui sont en fait provoqués par l'échec d'un test au début de la série et qui a laissé le monde en désordre pour les autres tests. Cette orientation en faveur des tests indépendants encourage les conceptions dotées d'un grand nombre d'objets simples. Chaque objet peut être testé rapidement de manière indépendante. Il en résulte de meilleures conceptions et des tests plus rapides.
PHPUnit part du postulat que la plupart des tests réussissent et qu'il n'est pas nécessaire de rendre compte des détails des tests réussis. Quand un test échoue, cela vaut la peine de le signaler et de détailler le problème. La grande majorité des tests doit réussir et aucun commentaire n'est intéressant si ce n'est le nombre de tests exécutés. Dans la pratique, ce postulat est implémenté dans les classes produisant les rapports et pas dans le noyau de PHPUnit. Quand les résultats d'une série de tests sont rapportés, vous voyez combien de tests ont été exécutés mais vous ne verrez les détails que pour ceux qui ont échoué.
Les tests sont supposés être à fine maille, ne testant qu'un seul aspect d'un seul objet. Par conséquent, la première fois qu'un test échoue, l'exécution du test s'interrompt et PHPUnit rapporte le problème. Tester en exécutant de nombreux petits tests est un art. Les tests à fine maille améliore la conception générale du système.
Lorsque vous testez un objet avec PHPUnit, vous ne le faites que via l'interface publique de l'objet. Tester en ne se basant que sur le comportement publiquement visible encourage à affronter et résoudre les problèmes de conception difficiles très tôt, avant que les conséquences d'une mauvaise conception ne puissent infecter de grandes parties du système.
PHPUnit doit être installé en utilisant l'installateur PEAR, la colonne vertébrale du dépôt d'extensions et d'applications PHP (PHP Extension and Application Repository) qui apporte un système de distribution pour les paquets PHP.
Selon votre distribution de système d'exploitation et/ou votre environnement PHP, vous pouvez avoir besoin d'installer PEAR ou de mettre à jour votre installation existante de PEAR avant de pouvoir suivre les instructions de ce chapitre.
sudo pear upgrade PEAR suffit habituellement pour
mettre à jour une installation PEAR existante. Le manuel PEAR
explique comment réaliser une nouvelle installation de PEAR.
PHPUnit 3.6 nécessite PHP 5.2.7 (ou ultérieur) mais PHP 5.3.9 (ou ultérieur) est fortement recommandé.
PHP_CodeCoverage, la bibliothèque qui est utilisée par PHPUnit pour rassembler et traiter les informations de couverture de code, dépend de Xdebug 2.0.5 (ou ultérieure) mais Xdebug 2.1.3 (ou ultérieur) est fortement recommandé.
Les deux commandes suivantes (que vous aurez peut-être à exécuter en tant que
super-administrateur, i.e. root )sont tout ce qui est nécessaire
pour installer PHPUnit en utilisant l'installateur PEAR:
pear config-set auto_discover 1pear install pear.phpunit.de/PHPUnit
Les paquets facultatifs suivants sont disponibles :
DbUnitPortage de DbUnit pour PHP/PHPUnit destiné à gérer les tests interagissant avec des bases de données.
Ce paquet peut être installé en utilisant la commande suivante :
pear install phpunit/DbUnitPHPUnit_SeleniumIntégration de Selenium RC pour PHPUnit.
Ce paquet peut être installé en utilisant la commande suivante :
pear install phpunit/PHPUnit_SeleniumPHPUnit_StoryLanceur de tests basés sur des histoires pour les développements dirigés par le comportement (Behavior-Driven Development) avec PHPUnit.
Ce paquet peut être installé en utilisant la commande suivante :
pear install phpunit/PHPUnit_StoryPHPUnit_TestListener_DBUSUn moniteur de tests qui envoie des événements à DBUS.
Ce paquet peut être installé en utilisant la commande suivante :
pear install phpunit/PHPUnit_TestListener_DBUSPHPUnit_TestListener_XHProfUn moniteur de tests qui utilise XHProf pour profiler automatiquement le code testé.
Ce paquet peut être installé en utilisant la commande suivante :
pear install phpunit/PHPUnit_TestListener_XHProfPHPUnit_TicketListener_FogbugzUn moniteur de tickets qui interagit avec l'API d'incidents de FogBugz.
Ce paquet peut être installé en utilisant la commande suivante :
pear install phpunit/PHPUnit_TicketListener_FogbugzPHPUnit_TicketListener_GitHubUn moniteur de tickets qui interagit avec l'API d'incidents de GitHub.
Ce paquet peut être installé en utilisant la commande suivante :
pear install phpunit/PHPUnit_TicketListener_GitHubPHPUnit_TicketListener_GoogleCodeUn moniteur de tickets qui interagit avec l'API d'incidents de Google Code.
Ce paquet peut être installé en utilisant la commande suivante :
pear install phpunit/PHPUnit_TicketListener_GoogleCodePHPUnit_TicketListener_TracUn moniteur de tickets qui interagit avec l'API d'incidents de Trac.
Ce paquet peut être installé en utilisant la commande suivante :
pear install phpunit/PHPUnit_TicketListener_TracPHP_InvokerUne classe utilitaire pour invoquer des appels avec un délai d'expiration. Ce paquet est nécessaire pour mettre en oeuvre des dépassements de délais pour les tests en mode strict.
Ce paquet peut être installé en utilisant la commande suivante :
pear install phpunit/PHP_Invoker
Après l'installation, vous trouverez les fichiers du code source de PHPUnit dans votre
répertoire local PEAR; le chemin d'accès est habituellement
/usr/lib/php/PHPUnit.
Exemple 4.1, « Tester des opérations de tableau avec PHPUnit » montre comment nous pouvons écrire des tests en utilisant PHPUnit pour contrôler les opérations PHP sur les tableaux. L'exemple introduit les conventions et les étapes de base pour écrire des tests avec PHPUnit:
Les tests pour une classe Classe vont dans une classe ClasseTest.
ClasseTest hérite (la plupart du temps) de PHPUnit_Framework_TestCase.
Les tests sont des méthodes publiques qui sont appelées test*.
Alternativement, vous pouvez utiliser l'annotation @test dans le bloc de documentation d'une méthode pour la marquer comme étant une méthode de test.
A l'intérieur des méthodes de test, des méthodes d'assertion telles que assertEquals() (voir la section intitulée « Assertions ») sont utilisées pour affirmer qu'une valeur constatée correspond à une valeur attendue.
Exemple 4.1. Tester des opérations de tableau avec PHPUnit
<?php
class PileTest extends PHPUnit_Framework_TestCase
{
public function testerPushEtPop()
{
$pile = array();
$this->assertEquals(0, count($pile));
array_push($pile, 'foo');
$this->assertEquals('foo', $pile[count($pile)-1]);
$this->assertEquals(1, count($pile));
$this->assertEquals('foo', array_pop($pile));
$this->assertEquals(0, count($pile));
}
}
?>
A chaque fois que vous avez la tentation de saisir quelque chose dans une
instruction | ||
| --Martin Fowler | ||
Les tests unitaires sont avant tout écrits comme étant une bonne pratique destinée à aider les développeurs à identifier et corriger les bugs, à refactoriser le code et à servir de documentation pour une unité du logiciel testé. Pour obtenir ces avantages, les tests unitaires doivent idéalement couvrir tous les chemins possibles du programme. Un test unitaire couvre usuellement un unique chemin particulier d'une seule fonction ou méthode. Cependant, une méthode de test n'est pas obligatoirement une entité encapsulée et indépendante. Souvent, il existe des dépendances implicites entre les méthodes de test, cachées dans l'implémentation du scénario d'un test. | ||
| --Adrian Kuhn et. al. | ||
PHPUnit gère la déclaration de dépendances explicites entre les méthodes de test. De telles dépendances ne définissent pas l'ordre dans lequel les méthodes de test doivent être exécutées, mais elles permettent l'envoi d'une instance d'un composant de test par un producteur à des consommateurs qui en dépendent.
Un producteur est une méthode de test qui cède ses éléments testées comme valeur de sortie.
Un consommateur est une méthode de test qui dépend d'un ou plusieurs producteurs et de leurs valeurs de retour.
Exemple 4.2, « Utiliser l'annotation @depends pour exprimer des dépendances » montre comment
utiliser l'annotation @depends pour exprimer des dépendances
entre des méthodes de test.
Exemple 4.2. Utiliser l'annotation @depends pour exprimer des dépendances
<?php
class PileTest extends PHPUnit_Framework_TestCase
{
public function testEmpty()
{
$pile = array();
$this->assertEmpty($pile);
return $pile;
}
/**
* @depends testEmpty
*/
public function testPush(array $pile)
{
array_push($pile, 'foo');
$this->assertEquals('foo', $pile[count($pile)-1]);
$this->assertNotEmpty($pile);
return $pile;
}
/**
* @depends testPush
*/
public function testPop(array $pile)
{
$this->assertEquals('foo', array_pop($pile));
$this->assertEmpty($pile);
}
}
?>
Dans l'exemple ci-dessus, le premier test, testVide(),
crée un nouveau tableau et affirme qu'il est vide. Le test renvoie ensuite
la fixture comme résultat. Le deuxième test, testPush(),
dépend de testVide() et reçoit le résultat de ce test
dont il dépend comme argument. Enfin, testPop()
dépend de testPush().
Pour localiser rapidement les défauts, nous voulons que notre attention soit retenue par les tests en échecs pertinents. C'est pourquoi PHPUnit saute l'exécution d'un test quand un test dont il dépend a échoué. Ceci améliore la localisation des défauts en exploitant les dépendances entre les tests comme montré dans Exemple 4.3, « Exploiter les dépendances entre les tests ».
Exemple 4.3. Exploiter les dépendances entre les tests
<?php
class DependencyFailureTest extends PHPUnit_Framework_TestCase
{
public function testUn()
{
$this->assertTrue(FALSE);
}
/**
* @depends testUn
*/
public function testDeux()
{
}
}
?>
phpunit --verbose DependencyFailureTest
PHPUnit 3.6.0 by Sebastian Bergmann.
FS
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) DependencyFailureTest::testUn
Failed asserting that false is true.
/home/sb/DependencyFailureTest.php:6
There was 1 skipped test:
1) DependencyFailureTest::testDeux
This test depends on "DependencyFailureTest::testUn" to pass.
FAILURES!
Tests: 1, Assertions: 1, Failures: 1, Skipped: 1.
Un test peut avoir plusieurs annotations @depends.
PHPUnit ne change pas l'ordre dans lequel les tests sont exécutés, vous
devez donc vous assurer que les dépendances d'un test peuvent effectivement
être utilisables avant que le test ne soit lancé.
Une méthode de test peut recevoir des arguments arbitraires. Ces arguments doivent
être fournis par une méthode fournisseuse de données (provider() dans
Exemple 4.4, « Utiliser un fournisseur de données qui renvoie un tableau de tableaux »).
La méthode fournisseuse de données à utiliser est indiquée dans l'annotation
@dataProvider annotation.
Une méthode fournisseuse de données doit être public et retourne, soit
un tableau de tableaux, soit un objet qui implémente l'interface Iterator
et renvoie un tableau pour chaque itération. Pour chaque tableau qui est une partie de
l'ensemble, la méthode de test sera appelée avec comme arguments le contenu du tableau.
Exemple 4.4. Utiliser un fournisseur de données qui renvoie un tableau de tableaux
<?php
class DataTest extends PHPUnit_Framework_TestCase
{
/**
* @dataProvider fournisseur
*/
public function testAdditionne($a, $b, $c)
{
$this->assertEquals($c, $a + $b);
}
public function fournisseur()
{
return array(
array(0, 0, 0),
array(0, 1, 1),
array(1, 0, 1),
array(1, 1, 3)
);
}
}
?>
phpunit DataTest
PHPUnit 3.6.0 by Sebastian Bergmann.
...F
Time: 0 seconds, Memory: 5.75Mb
There was 1 failure:
1) DataTest::testAdditionne with data set #3 (1, 1, 3)
Failed asserting that 2 matches expected 3.
/home/sb/DataTest.php:9
FAILURES!
Tests: 4, Assertions: 4, Failures: 1.Exemple 4.5. Utiliser un fournisseur de données qui renvoie un objet Iterator
<?php
require 'CsvFileIterator.php';
class DataTest extends PHPUnit_Framework_TestCase
{
/**
* @dataProvider fournisseur
*/
public function testAdditionne($a, $b, $c)
{
$this->assertEquals($c, $a + $b);
}
public function fournisseur()
{
return new CsvFileIterator('data.csv');
}
}
?>
phpunit DataTest
PHPUnit 3.6.0 by Sebastian Bergmann.
...F
Time: 0 seconds, Memory: 5.75Mb
There was 1 failure:
1) DataTest::testAdditionne with data set #3 ('1', '1', '3')
Failed asserting that 2 matches expected '3'.
/home/sb/DataTest.php:11
FAILURES!
Tests: 4, Assertions: 4, Failures: 1.Exemple 4.6. La classe CsvFileIterator
<?php
class CsvFileIterator implements Iterator {
protected $fichier;
protected $key = 0;
protected $current;
public function __construct($fichier) {
$this->fichier = fopen($fichier, 'r');
}
public function __destruct() {
fclose($this->fichier);
}
public function rewind() {
rewind($this->fichier);
$this->current = fgetcsv($this->fichier);
$this->key = 0;
}
public function valid() {
return !feof($this->fichier);
}
public function key() {
return $this->key;
}
public function current() {
return $this->current;
}
public function next() {
$this->current = fgetcsv($this->fichier);
$this->key++;
}
}
?>
Quand un test reçoit des entrées à la fois d'une méthode @dataProvider
et d'un ou plusieurs tests dont il @depends, les arguments provenant du
fournisseur de données arriveront avant ceux des tests dont il dépend.
Exemple 4.7, « Utiliser l'annotation @expectedException »
montre comment utiliser l'annotation @expectedException pour tester
si une exception est levée à l'intérieur du code testé.
Exemple 4.7. Utiliser l'annotation @expectedException
<?php
class ExceptionTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException InvalidArgumentException
*/
public function testException()
{
}
}
?>
phpunit ExceptionTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 4.75Mb
There was 1 failure:
1) ExceptionTest::testException
Expected exception InvalidArgumentException
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
Additionnellement, vous pouvez utiliser @expectedExceptionMessage
et @expectedExceptionCode en combinaison de
@expectedException pour tester le message d'exception et le code
d'exception comme montré dans
Exemple 4.8, « Utiliser les annotations @expectedExceptionMessage et @expectedExceptionCode ».
Exemple 4.8. Utiliser les annotations @expectedExceptionMessage et @expectedExceptionCode
<?php
class ExceptionTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessage Message correct
*/
public function testExceptionPossedeLeBonMessage()
{
throw new InvalidArgumentException('Un message', 10);
}
/**
* @expectedException InvalidArgumentException
* @expectedExceptionCode 20
*/
public function testExceptionPossedeLeBonCode()
{
throw new InvalidArgumentException('Un message', 10);
}
}
?>
phpunit ExceptionTest
PHPUnit 3.6.0 by Sebastian Bergmann.
FF
Time: 0 seconds, Memory: 3.00Mb
There were 2 failures:
1) ExceptionTest::testExceptionPossedeLeBonMessage
Failed asserting that exception message 'Un Message' contains 'Message correct'.
2) ExceptionTest::testExceptionPossedeLeBonCode
Failed asserting that expected exception code 20 is equal to 10.
FAILURES!
Tests: 2, Assertions: 4, Failures: 2.
Alternativement, vous pouvez utiliser la méthode setExpectedException()
pour indiquer l'exception attendue comme montré dans Exemple 4.9, « Attendre une exception qui doit être levée par le code testé ».
Exemple 4.9. Attendre une exception qui doit être levée par le code testé
<?php
class ExceptionTest extends PHPUnit_Framework_TestCase
{
public function testException()
{
$this->setExpectedException('InvalidArgumentException');
}
public function testExceptionPossedeLeBonMessage()
{
$this->setExpectedException(
'InvalidArgumentException', 'Message correct'
);
throw new InvalidArgumentException('Un message', 10);
}
public function testExceptionPossedeLeBonCode()
{
$this->setExpectedException(
'InvalidArgumentException', 'Message correct', 20
);
throw new InvalidArgumentException('Message correct', 10);
}
}?>
phpunit ExceptionTest
PHPUnit 3.6.6 by Sebastian Bergmann.
FFF
Time: 0 seconds, Memory: 3.00Mb
There were 3 failures:
1) ExceptionTest::testException
Expected exception InvalidArgumentException
2) ExceptionTest::testExceptionPossedeLeBonMessage
Failed asserting that exception message 'Un message' contains 'Message correct'.
3) ExceptionTest::testExceptionPossedeLeBonCode
Failed asserting that expected exception code 20 is equal to 10.
FAILURES!
Tests: 3, Assertions: 6, Failures: 3.Tableau 4.1, « Méthodes pour tester des exceptions » montre les méthodes fournies pour tester des exceptions.
Tableau 4.1. Méthodes pour tester des exceptions
| Méthode | Signification |
|---|---|
void setExpectedException(string $nomDeLException[, string $messageDeLException = '', integer $codeDeLException = NULL]) | Indiquer le $nomDeLException attendue, le $messageDeLException et le $codeDeLException. |
String getExpectedException() | Retourne le nom de l'exception attendue. |
Vous pouvez également utiliser l'approche montrée dans Exemple 4.10, « Approche alternative pour tester des exceptions » pour tester des exceptions.
Exemple 4.10. Approche alternative pour tester des exceptions
<?php
class ExceptionTest extends PHPUnit_Framework_TestCase {
public function testException() {
try {
// ... Code qui devrait lever une exception ...
}
catch (InvalidArgumentException $attendu) {
return;
}
$this->fail('Une exception attendue n'a pas été levée.');
}
}
?>
Si le code qui devrait lever une exception dans Exemple 4.10, « Approche alternative pour tester des exceptions »
ne lève pas l'exception attendue, l'appel induit à
fail() va interrompre le test et signaler un problème pour ce test.
Si l'exception attendue est levée, le bloc catch
sera exécuté et le test s'achèvera avec succès.
Par défaut, PHPUnit convertit les erreurs, avertissements et remarques PHP qui sont émises lors de l'exécution d'un test en exception. En utilisant ces exceptions, vous pouvez, par exemple, attendre d'un test qu'il produise une erreur PHP comme montré dans Exemple 4.11, « Attendre une erreur PHP en utilisant @expectedException ».
Exemple 4.11. Attendre une erreur PHP en utilisant @expectedException
<?php
class ExpectedErrorTest extends PHPUnit_Framework_TestCase
{
/**
@expectedException PHPUnit_Framework_Error
*/
public function testEchecInclude()
{
include 'fichier_qui_n_existe_pas.php';
}
}
?>
phpunit ExpectedErrorTest
PHPUnit 3.6.6 by Sebastian Bergmann.
.
Time: 0 seconds, Memory: 5.25Mb
OK (1 test, 1 assertion)
PHPUnit_Framework_Error_Notice et
PHPUnit_Framework_Error_Warning représentent respectivement
les remarques et les avertissements PHP.
Vous devriez être aussi précis que possible lorsque vous testez des exceptions.
Tester avec des classes qui sont trop génériques peut conduire à des effets de
bord indésirables. C'est pourquoi tester la présence de la classe
Exception avec @expectedException ou
setExpectedException() n'est plus autorisé.
Quand les tests s'appuient sur des fonctions php qui déclenchent des erreurs
comme fopen, il peut parfois être utile d'utiliser la
suppression d'erreur lors du test. Ceci permet de contrôler les valeurs de retour
en supprimant les remarques qui auraient conduit à une
PHPUnit_Framework_Error_Notice de phpunit.
Exemple 4.12. Tester des valeurs de retour d'un code source qui utilise des erreurs PHP
<?php
class ErrorSuppressionTest extends PHPUnit_Framework_TestCase
{
public function testEcritureFichier() {
$writer = new FileWriter;
$this->assertFalse(@$writer->ecrit('/non-accessible-en-ecriture/fichier', 'texte'));
}
}
class FileWriter
{
public function ecrit($fichier, $contenu) {
$fichier = fopen($fichier, 'w');
if($fichier == false) {
return false;
}
// ...
}
}
?>
phpunit ErrorSuppressionTest
PHPUnit 3.6.0 by Sebastian Bergmann.
.
Time: 1 seconds, Memory: 5.25Mb
OK (1 test, 1 assertion)
Sans la suppression d'erreur, le test échouerait à rapporter
fopen(/non-accessible-en-ecriture/fichier): failed to open stream:
No such file or directory.
Quelquefois, vous voulez affirmer que l'exécution d'une méthode, par
exemple, produit une sortie écran donnée (via echo ou
print, par exemple). La classe
PHPUnit_Framework_TestCase utilise la fonctionnalité de
en tampon de PHP mise en tampon de la sortie écran
de PHP pour fournir la fonctionnalité qui est nécessaire pour cela.
Exemple 4.13, « Tester la sortie écran d'une fonction ou d'une méthode »
montre comment utiliser la méthode expectOutputString() pour
indiquer la sortie écran attendue. Si la sortie écran attendue n'est pas générée, le test
sera compté comme étant en échec.
Exemple 4.13. Tester la sortie écran d'une fonction ou d'une méthode
<?php
class OutputTest extends PHPUnit_Framework_TestCase
{
public function testFooAttenduFooObtenu()
{
$this->expectOutputString('foo');
print 'foo';
}
public function testBarAttenduBazAttendu()
{
$this->expectOutputString('bar');
print 'baz';
}
}
?>
phpunit OutputTest
PHPUnit 3.6.0 by Sebastian Bergmann.
.F
Time: 0 seconds, Memory: 5.75Mb
There was 1 failure:
1) OutputTest::testBarAttenduBazObtenu
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'bar'
+'baz'
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.Tableau 4.2, « Méthodes pour tester les sorties écran » montre les méthodes fournies pour tester les sorties écran
Tableau 4.2. Méthodes pour tester les sorties écran
| Méthode | Signification |
|---|---|
void expectOutputRegex(string $expressionRationnelle) | Indique que l'on s'attend à ce que la sortie écran corresponde à une $expressionRationnelle. |
void expectOutputString(string $attenduString) | Indique que l'on s'attend que la sortie écran soit égale à une $chaineDeCaracteresAttendue. |
bool setOutputCallback(callable $callback) | Configure une fonction de rappel (callback) qui est utilisée, par exemple, formater la sortie écran effective. |
Merci de noter que PHPUnit absorbe toutes les sorties écran qui sont émises lors de l'exécution d'un test. En mode strict, un test qui produit une sortie écran échouera.
Cette section liste les diverses méthodes d'assertion qui sont disponibles.
assertArrayHasKey(mixed $clef, array $tableau[, string $message = ''])
Rapporte une erreur identifiée par un $message si le $tableau ne contient pas la $clef.
assertArrayNotHasKey() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.14. Utilisation de assertArrayHasKey()
<?php
class TableauPossedeUneClefTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertArrayHasKey('foo', array('bar' => 'baz'));
}
}
?>
phpunit TableauPossedeUneClefTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) TableauPossedeUneClefTest::testEchec
Failed asserting that an array has the key 'foo'.
/home/sb/TableauPossedeUneClefTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertClassHasAttribute(string $nomAttribut, string $nomClasse[, string $message = ''])
Rapporte une erreur identifiée par un $message si $nomClasse::nomAttribut n'existe pas.
assertClassNotHasAttribute() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.15. Utilisation de assertClassHasAttribute()
<?php
class ClassePossedeUnAttributTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertClassHasAttribute('foo', 'stdClass');
}
}
?>
phpunit ClassePossedeUnAttributTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 4.75Mb
There was 1 failure:
1) ClassePossedeUnAttributTest::testEchec
Failed asserting that class "stdClass" has attribute "foo".
/home/sb/ClassePossedeUnAttributTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertClassHasStaticAttribute(string $nomAttribut, string $nomClasse[, string $message = ''])
Rapporte une erreur identifiée par un $message si $nomClasse::nomAttribut n'existe pas.
assertClassNotHasStaticAttribute() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.16. Utilisation de assertClassHasStaticAttribute()
<?php
class ClassePossedeUnAttributStatiqueTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertClassHasStaticAttribute('foo', 'stdClass');
}
}
?>
phpunit ClassePossedeUnAttributStatiqueTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 4.75Mb
There was 1 failure:
1) ClassHasStaticAttributeTest::testEchec
Failed asserting that class "stdClass" has static attribute "foo".
/home/sb/ClassePossedeUnAttributStatiqueTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertContains(mixed $aiguille, Iterator|array $meuleDeFoin[, string $message = ''])
Rapporte une erreur identifiée par un $message si $aiguille n'est pas un élément de $meuleDeFoin.
assertNotContains() est l'inverse de cette assertion et prend les mêmes arguments.
assertAttributeContains() et assertAttributeNotContains() sont des enrobeurs de commodité qui utilisent l'attribut public, protected ou private d'une classe ou d'un objet comme meuleDeFoin.
Exemple 4.17. Utilisation de assertContains()
<?php
class ContainsTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertContains(4, array(1, 2, 3));
}
}
?>
phpunit ContainsTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) ContainsTest::testEchec
Failed asserting that an array contains 4.
/home/sb/ContainsTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertContains(string $aiguille, string $meuleDeFoin[, string $message = ''])
Rapporte une erreur identifiée par un $message si $aiguille n'est pas un sous chaîne de $meuleDeFoin.
Exemple 4.18. Utilisation de assertContains()
<?php
class ContainsTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertContains('baz', 'foobar');
}
}
?>
phpunit ContainsTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) ContainsTest::testEchec
Failed asserting that 'foobar' contains "baz".
/home/sb/ContainsTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertContainsOnly(string $type, Iterator|array $meuleDeFoin[, boolean $estUnTypeNatif = NULL, string $message = ''])
Rapporte une erreur identifiée par le $message si $meuleDeFoin ne contient pas que des variables du type $type.
$estUnTypeNatif est un drapeau qui indique si $type est un type natif de PHP ou pas.
assertNotContainsOnly() est l'inverse de cette assertion et prend les mêmes arguments.
assertAttributeContainsOnly() et assertAttributeNotContainsOnly() sont des enrobeurs de commodité qui utilisent un attribut public, protected ou private d'une classe ou d'un objet en tant que valeur constatée.
Exemple 4.19. Utilisation de assertContainsOnly()
<?php
class ContainsOnlyTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertContainsOnly('string', array('1', '2', 3));
}
}
?>
phpunit ContainsOnlyTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) ContainsOnlyTest::testEchec
Failed asserting that Array (
0 => '1'
1 => '2'
2 => 3
) contains only values of type "string".
/home/sb/ContainsOnlyTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertCount($nombreAttendu, $meuleDeFoin[, string $message = ''])
Rapporte une erreur identifiée par $message si le nombre d'éléments dans $meuleDeFoin n'est pas $nombreAttendu.
assertNotCount() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.20. Utilisation de assertCount()
<?php
class CountTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertCount(0, array('foo'));
}
}
?>
phpunit CountTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 4.75Mb
There was 1 failure:
1) CountTest::testEchec
Failed asserting that actual size 1 matches expected size 0.
/home/sb/CountTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertEmpty(mixed $constate[, string $message = ''])
Rapporte une erreur identifiée par $message si $constate n'est pas vide.
assertNotEmpty() est l'inverse de cette assertion et prend les mêmes arguments.
assertAttributeEmpty() et assertAttributeNotEmpty() sont des enrobeurs de commodité qui peuvent être appliqués à un attribut public, protected ou private d'une classe ou d'un objet.
Exemple 4.21. Utilisation de assertEmpty()
<?php
class VideTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertEmpty(array('foo'));
}
}
?>
phpunit VideTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 4.75Mb
There was 1 failure:
1) VideTest::testEchec
Failed asserting that an array is empty.
/home/sb/VideTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertEqualXMLStructure(DOMElement $elementAttendu, DOMElement $elementConstate[, boolean $verifieAttributs = FALSE, string $message = ''])
Rapporte une erreur identifiée par $message si la structure XML de l'élément DOMElement de $elementConstate n'est pas égale à la structure de l'élément DOMElement de $elementAttendu.
Exemple 4.22. Utilisation de assertEqualXMLStructure()
<?php
class StructuresXMLSontEgalesTest extends PHPUnit_Framework_TestCase
{
public function testEchecAvecDifferentsNomsdeNoeud()
{
$attendu = new DOMElement('foo');
$constate = new DOMElement('bar');
$this->assertEqualXMLStructure($attendu, $constate);
}
public function testEchecAvecDifferentsAttributsDeNoeud()
{
$attendu = new DOMDocument;
$attendu->loadXML('<foo bar="true" />');
$constate = new DOMDocument;
$constate->loadXML('<foo/>');
$this->assertEqualXMLStructure(
$attendu->firstChild, $constate->firstChild, TRUE
);
}
public function testEchecAvecDecompteDifferentdesNoeudsFils()
{
$attendu = new DOMDocument;
$attendu->loadXML('<foo><bar/><bar/><bar/></foo>');
$constate = new DOMDocument;
$constate->loadXML('<foo><bar/></foo>');
$this->assertEqualXMLStructure(
$attendu->firstChild, $constate->firstChild
);
}
public function testEchecAvecDesNoeudsFilsDifferents()
{
$attendu = new DOMDocument;
$attendu->loadXML('<foo><bar/><bar/><bar/></foo>');
$constate = new DOMDocument;
$constate->loadXML('<foo><baz/><baz/><baz/></foo>');
$this->assertEqualXMLStructure(
$attendu->firstChild, $constate->firstChild
);
}
}
?>
phpunit StructuresXMLSontEgalesTest
PHPUnit 3.6.0 by Sebastian Bergmann.
FFFF
Time: 0 seconds, Memory: 5.75Mb
There were 4 failures:
1) StructuresXMLSontEgalesTest::testEchecAvecDifferentsNomsdeNoeud
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'foo'
+'bar'
/home/sb/StructuresXMLSontEgalesTest.php:9
2) StructuresXMLSontEgalesTest::testEchecAvecDifferentsAttributsDeNoeud
Number of attributes on node "foo" does not match
Failed asserting that 0 matches expected 1.
/home/sb/StructuresXMLSontEgalesTest.php:22
3) StructuresXMLSontEgalesTest::testEchecAvecDecompteDifferentdesNoeudsFils
Number of child nodes of "foo" differs
Failed asserting that 1 matches expected 3.
/home/sb/StructuresXMLSontEgalesTest.php:35
4) StructuresXMLSontEgalesTest::testEchecAvecDesNoeudsFilsDifferents
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'bar'
+'baz'
/home/sb/StructuresXMLSontEgalesTest.php:48
FAILURES!
Tests: 4, Assertions: 8, Failures: 4.assertEquals(mixed $attendu, mixed $constate[, string $message = ''])
Rapporte une erreur identifiée par le $message si les deux variables $attendu et $constate ne sont pas égales.
assertNotEquals() est l'inverse de cette assertion et prend les mêmes paramètres.
assertAttributeEquals() et assertAttributeNotEquals() sont des enrobeurs de commodité qui utilisent un attribut public, protected ou private d'une classe ou d'un objet comme valeur constatée.
Exemple 4.23. Utilisation de assertEquals()
<?php
class EqualsTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertEquals(1, 0);
}
public function testEchec2()
{
$this->assertEquals('bar', 'baz');
}
public function testEchec3()
{
$this->assertEquals("foo\nbar\nbaz\n", "foo\nbah\nbaz\n");
}
}
?>
phpunit EqualsTest
PHPUnit 3.6.0 by Sebastian Bergmann.
FFF
Time: 0 seconds, Memory: 5.25Mb
There were 3 failures:
1) EqualsTest::testEchec
Failed asserting that 0 matches expected 1.
/home/sb/EqualsTest.php:6
2) EqualsTest::testEchec2
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'bar'
+'baz'
/home/sb/EqualsTest.php:11
3) EqualsTest::testEchec3
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
'foo
-bar
+bah
baz
'
/home/sb/EqualsTest.php:16
FAILURES!
Tests: 3, Assertions: 3, Failures: 3.Des comparaisons plus spécifiques sont utilisées pour des types d'arguments $attendu et $constate plus spécifiques, voir ci-dessous.
assertEquals(float $attendu, float $constate[, string $message = '', float $delta = 0])
Rapporte une erreur identifiée par le $message si les deux nombres à virgule flottante $attendu et $constate ne sont pas à moins de $delta l'un de l'autre.
Merci de lire comparing floating-point numbers pour comprendre pourquoi $delta est indispensable.
Exemple 4.24. Utilisation de assertEquals() avec des nombres à virgule flottante
<?php
class EqualsTest extends PHPUnit_Framework_TestCase
{
public function testSucces()
{
$this->assertEquals(1.0, 1.1, '', 0.2);
}
public function testEchec()
{
$this->assertEquals(1.0, 1.1);
}
}
?>
phpunit EqualsTest
PHPUnit 3.6.0 by Sebastian Bergmann.
.F
Time: 0 seconds, Memory: 5.75Mb
There was 1 failure:
1) EqualsTest::testEchec
Failed asserting that 1.1 matches expected 1.0.
/home/sb/EqualsTest.php:11
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.assertEquals(DOMDocument $attendu, DOMDocument $constate[, string $message = ''])
Rapporte une erreur identifiée par le $message si la forme canonique non commentée des documents XML représentés par les deux objets DOMDocument objects $attendu et $constate ne sont pas égaux.
Exemple 4.25. Utilisation de assertEquals() avec des objets DOMDocument
<?php
class EqualsTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$attendu = new DOMDocument;
$attendu->loadXML('<foo><bar/></foo>');
$constate = new DOMDocument;
$constate->loadXML('<bar><foo/></bar>');
$this->assertEquals($attendu, $constate);
}
}
?>
phpunit EqualsTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) EqualsTest::testEchec
Failed asserting that two DOM documents are equal.
--- Expected
+++ Actual
@@ @@
<?xml version="1.0"?>
-<foo>
- <bar/>
-</foo>
+<bar>
+ <foo/>
+</bar>
/home/sb/EqualsTest.php:12
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertEquals(object $attendu, object $constate[, string $message = ''])
Rapporte une erreur identifiée par le $message si les deux objets $attendu et $constate ne possède pas des valeurs d'attribut égales.
Exemple 4.26. Utilisation de assertEquals() avec des objets
<?php
class EqualsTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$attendu = new stdClass;
$attendu->foo = 'foo';
$attendu->bar = 'bar';
$constate = new stdClass;
$constate->foo = 'bar';
$constate->baz = 'bar';
$this->assertEquals($attendu, $constate);
}
}
?>
phpunit EqualsTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) EqualsTest::testEchec
Failed asserting that two objects are equal.
--- Expected
+++ Actual
@@ @@
stdClass Object (
- 'foo' => 'foo'
- 'bar' => 'bar'
+ 'foo' => 'bar'
+ 'baz' => 'bar'
)
/home/sb/EqualsTest.php:14
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertEquals(array $attendu, array $constate[, string $message = ''])
Rapporte une erreur identifiée par le $message si les deux tableaux $attendu et $constate ne sont pas égaux.
Exemple 4.27. Utilisation de assertEquals() avec des tableaux
<?php
class EqualsTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertEquals(array('a', 'b', 'c'), array('a', 'c', 'd'));
}
}
?>
phpunit EqualsTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) EqualsTest::testEchec
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
Array (
0 => 'a'
- 1 => 'b'
- 2 => 'c'
+ 1 => 'c'
+ 2 => 'd'
)
/home/sb/EqualsTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertFalse(bool $condition[, string $message = ''])
Rapporte une erreur identifiée par le $message si la $condition est TRUE (VRAIE).
Exemple 4.28. Utilisation de assertFalse()
<?php
class FalseTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertFalse(TRUE);
}
}
?>
phpunit FalseTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) FalseTest::testEchec
Failed asserting that true is false.
/home/sb/FalseTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertFileEquals(string $attendu, string $constate[, string $message = ''])
Rapporte une erreur identifiée par le $message si le fichier identifié par $attendu ne possède pas le même contenu que le fichier identifié par $constate.
assertFileNotEquals() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.29. Utilisation de assertFileEquals()
<?php
class FileEqualsTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertFileEquals('/home/sb/attendu', '/home/sb/constate');
}
}
?>
phpunit FileEqualsTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) FileEqualsTest::testEchec
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'contenu attendu
+'contenu constaté
'
/home/sb/FileEqualsTest.php:6
FAILURES!
Tests: 1, Assertions: 3, Failures: 1.assertFileExists(string $nomfichier[, string $message = ''])
Rapporte une erreur identifiée par le $message si le fichier désigné par $nomfichier n'existe pas.
assertFileNotExists() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.30. Utilisation de assertFileExists()
<?php
class FileExistsTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertFileExists('/chemin/vers/fichier');
}
}
?>
phpunit FileExistsTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 4.75Mb
There was 1 failure:
1) FileExistsTest::testEchec
Failed asserting that file "/chemin/vers/fichier" exists.
/home/sb/FileExistsTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertGreaterThan(mixed $attendu, mixed $constate[, string $message = ''])
Rapporte une erreur identifiée par le $message si la valeur de $constate n'est pas supérieure à la valeur de $attendu.
assertAttributeGreaterThan() est un enrobeur de commodité qui utilise un attribut public, protected ou private d'une classe ou d'un objet comme valeur constatée.
Exemple 4.31. Utilisation de assertGreaterThan()
<?php
class GreaterThanTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertGreaterThan(2, 1);
}
}
?>
phpunit GreaterThanTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) GreaterThanTest::testEchec
Failed asserting that 1 is greater than 2.
/home/sb/GreaterThanTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertGreaterThanOrEqual(mixed $attendu, mixed $constate[, string $message = ''])
Rapporte une erreur identifiée par le $message si la valeur de $constate n'est pas supérieure ou égale à la valeur de $attendu.
assertAttributeGreaterThanOrEqual() est un enrobeur de commodité qui utilise un attribut public, protected ou private d'une classe ou d'un objet comme valeur constatée.
Exemple 4.32. Utilisation de assertGreaterThanOrEqual()
<?php
class GreatThanOrEqualTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertGreaterThanOrEqual(2, 1);
}
}
?>
phpunit GreaterThanOrEqualTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) GreatThanOrEqualTest::testEchec
Failed asserting that 1 is equal to 2 or is greater than 2.
/home/sb/GreaterThanOrEqualTest.php:6
FAILURES!
Tests: 1, Assertions: 2, Failures: 1.assertInstanceOf($attendu, $constate[, $message = ''])
Rapporte une erreur identifiée par le $message si $constate n'est pas une instance de $attendu.
assertNotInstanceOf() est l'inverse de cette assertion et prend les mêmes arguments.
assertAttributeInstanceOf() et assertAttributeNotInstanceOf() sont des enrobeurs de commodité qui peuvent être appliqué à un attribut public, protected ou private d'une classe ou d'un objet.
Exemple 4.33. Utilisation de assertInstanceOf()
<?php
class InstanceOfTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertInstanceOf('RuntimeException', new Exception);
}
}
?>
phpunit InstanceOfTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) InstanceOfTest::testEchec
Failed asserting that Exception Object (...) is an instance of class "RuntimeException".
/home/sb/InstanceOfTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertInternalType($attendu, $constate[, $message = ''])
Rapporte une erreur identifiée par le $message si $constate n'est pas du type $attendu.
assertNotInternalType() est l'inverse de cette assertion et prend les mêmes arguments.
assertAttributeInternalType() et assertAttributeNotInternalType() sont des enrobeurs de commodité qui peuvent être appliqués à un attribut public, protected ou private d'une classe ou d'un objet.
Exemple 4.34. Utilisation de assertInternalType()
<?php
class InternalTypeTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertInternalType('string', 42);
}
}
?>
phpunit InternalTypeTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) InternalTypeTest::testEchec
Failed asserting that 42 is of type "string".
/home/sb/InternalTypeTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertLessThan(mixed $attendu, mixed $constate[, string $message = ''])
Rapporte une erreur identifiée par le $message si la valeur de $constate n'est pas inférieure à la valeur de $attendu.
assertAttributeLessThan() est un enrobeur de commodité qui utilise un attribut public, protected ou private d'une classe ou d'un objet comme valeur constatée.
Exemple 4.35. Utilisation de assertLessThan()
<?php
class LessThanTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertLessThan(1, 2);
}
}
?>
phpunit LessThanTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) LessThanTest::testEchec
Failed asserting that 2 is less than 1.
/home/sb/LessThanTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertLessThanOrEqual(mixed $attendu, mixed $constate[, string $message = ''])
Rapporte une erreur identifiée par le $message si la valeur de $constate n'est pas inférieure ou égale à la valeur de $attendu.
assertAttributeLessThanOrEqual() est un enrobeur de commodité qui utilise un attribut public, protected ou private d'une classe ou d'un objet comme valeur attendue.
Exemple 4.36. Utilisation de assertLessThanOrEqual()
<?php
class LessThanOrEqualTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertLessThanOrEqual(1, 2);
}
}
?>
phpunit LessThanOrEqualTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) LessThanOrEqualTest::testEchec
Failed asserting that 2 is equal to 1 or is less than 1.
/home/sb/LessThanOrEqualTest.php:6
FAILURES!
Tests: 1, Assertions: 2, Failures: 1.assertNull(mixed $variable[, string $message = ''])
Rapporte une erreur identifiée par le $message si $variable n'est pas NULL.
assertNotNull() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.37. Utilisation de assertNull()
<?php
class NullTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertNull('foo');
}
}
?>
phpunit NotNullTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) NullTest::testEchec
Failed asserting that 'foo' is null.
/home/sb/NotNullTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertObjectHasAttribute(string $nomAttribut, object $objet[, string $message = ''])
Rapporte une erreur identifiée par le $message si $objet->nomAttribut n'existe pas.
assertObjectNotHasAttribute() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.38. Utilisation de assertObjectHasAttribute()
<?php
class ObjectHasAttributeTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertObjectHasAttribute('foo', new stdClass);
}
}
?>
phpunit ObjectHasAttributeTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 4.75Mb
There was 1 failure:
1) ObjectHasAttributeTest::testEchec
Failed asserting that object of class "stdClass" has attribute "foo".
/home/sb/ObjectHasAttributeTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertRegExp(string $motif, string $chaine[, string $message = ''])
Rapporte une erreur identifiée par le $message si $chaine ne correspond pas à l'expression rationnelle $motif.
assertNotRegExp() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.39. Utilisation de assertRegExp()
<?php
class RegExpTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertRegExp('/foo/', 'bar');
}
}
?>
phpunit RegExpTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) RegExpTest::testEchec
Failed asserting that 'bar' matches PCRE pattern "/foo/".
/home/sb/RegExpTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertStringMatchesFormat(string $format, string $chaine[, string $message = ''])
Rapporte une erreur identifiée par le $message si la chaîne $chaine ne correspond pas à la chaîne de $format.
assertStringNotMatchesFormat() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.40. Utilisation de assertStringMatchesFormat()
<?php
class StringMatchesFormatTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertStringMatchesFormat('%i', 'foo');
}
}
?>
phpunit StringMatchesFormatTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) StringMatchesFormatTest::testEchec
Failed asserting that 'foo' matches PCRE pattern "/^[+-]?\d+$/s".
/home/sb/StringMatchesFormatTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.La chaîne de format peut contenir les conteneurs suivants:
%e: Représente un séparateur de répertoire, par exemple / sur Linux.
%s: Un ou plusieurs caractères quelconque (y compris des espaces) à l'exception du caractère fin de ligne.
%S: Zéro ou plusieurs caractères quelconque (y compris des espaces) à l'exception du caractère fin de ligne.
%a: Un ou plusieurs caractères quelconque (y compris des espaces) y compris les caractères fin de ligne.
%A: Zéro ou plusieurs caractères quelconque (y compris des espaces) y compris les caractères fin de ligne.
%w: Zéro ou plusieurs espaces.
%i: Une valeur entière signée, par exemple +3142, -3142.
%d: Une valeur entière non signée, par exemple 123456.
%x: Un ou plusieurs caractères hexadécimaux. C'est-à-dire des caractères dans la plage 0-9, a-f, A-F.
%f: Un nombre en virgule flottante, par exemple: 3.142, -3.142, 3.142E-10, 3.142e+10.
%c: Un unique caractère de n'importe quelle sorte.
assertStringMatchesFormatFile(string $fichierFormat, string $chaine[, string $message = ''])
Rapporte une erreur identifiée par le $message si la chaîne $chaine ne correspond pas au contenu de $fichierFormat.
assertStringNotMatchesFormatFile() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.41. Utilisation de assertStringMatchesFormatFile()
<?php
class StringMatchesFormatFileTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertStringMatchesFormatFile('/chemin/vers/attendu.txt', 'foo');
}
}
?>
phpunit StringMatchesFormatFileTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) StringMatchesFormatFileTest::testEchec
Failed asserting that 'foo' matches PCRE pattern "/^[+-]?\d+
$/s".
/home/sb/StringMatchesFormatFileTest.php:6
FAILURES!
Tests: 1, Assertions: 2, Failures: 1.assertSame(mixed $attendu, mixed $constate[, string $message = ''])
Rapporte une erreur identifiée par le $message si les deux variables $attendu et $constate n'ont pas le même type et la même valeur.
assertNotSame() est l'inverse de cette assertion et prend les mêmes arguments.
assertAttributeSame() et assertAttributeNotSame() sont des enrobeurs de commodité qui utilisent un attribut public, protected ou private d'une classe ou d'un objet comme valeur constatée.
Exemple 4.42. Utilisation de assertSame()
<?php
class SameTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertSame('2204', 2204);
}
}
?>
phpunit SameTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) SameTest::testEchec
Failed asserting that 2204 is identical to '2204'.
/home/sb/SameTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertSame(object $attendu, object $constate[, string $message = ''])
Rapporte une erreur identifiée par le $message si les deux variables $attendu et $constate ne référence pas le même objet.
Exemple 4.43. Utilisation de assertSame() avec des objets
<?php
class SameTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertSame(new stdClass, new stdClass);
}
}
?>
phpunit SameTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 4.75Mb
There was 1 failure:
1) SameTest::testEchec
Failed asserting that two variables reference the same object.
/home/sb/SameTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertSelectCount(array $selecteur, integer $nombre, mixed $constate[, string $message = '', boolean $isHtml = TRUE])
Rapporte une erreur identifiée par le $message si le sélecteur CSS $selecteur ne correspond pas à $nombre éléments du noeud DOM $constate.
$nombre peut avoir l'un des types suivants :
booléen: présuppose la présence d'éléments correspondant au sélecteur (TRUE) ou l'absence d'éléments (FALSE).nombre entier: présuppose le nombre d'éléments.tableau: présuppose que le nombre sera dans la plage indiquée en utilisant <, >, <= et >= comme clefs.Exemple 4.44. Utilisation de assertSelectCount()
<?php
class SelectCountTest extends PHPUnit_Framework_TestCase
{
protected function setUp()
{
$this->xml = new DomDocument;
$this->xml->loadXML('<foo><bar/><bar/><bar/></foo>');
}
public function testAbsenceEchec()
{
$this->assertSelectCount('foo bar', FALSE, $this->xml);
}
public function testPresenceEchec()
{
$this->assertSelectCount('foo baz', TRUE, $this->xml);
}
public function testCompteExactEchec()
{
$this->assertSelectCount('foo bar', 5, $this->xml);
}
public function testPlageEchec()
{
$this->assertSelectCount('foo bar', array('>'=>6, '<'=>8), $this->xml);
}
}
?>
phpunit SelectCountTest
PHPUnit 3.6.0 by Sebastian Bergmann.
FFFF
Time: 0 seconds, Memory: 5.50Mb
There were 4 failures:
1) SelectCountTest::testAbsenceEchec
Failed asserting that true is false.
/home/sb/SelectCountTest.php:12
2) SelectCountTest::testPresenceEchec
Failed asserting that false is true.
/home/sb/SelectCountTest.php:17
3) SelectCountTest::testCompteExactEchec
Failed asserting that 3 matches expected 5.
/home/sb/SelectCountTest.php:22
4) SelectCountTest::testPlageEchec
Failed asserting that false is true.
/home/sb/SelectCountTest.php:27
FAILURES!
Tests: 4, Assertions: 4, Failures: 4.assertSelectEquals(array $selecteur, string $contenu, integer $nombre, mixed $constate[, string $message = '', boolean $isHtml = TRUE])
Rapporte une erreur identifiée par le $message si le sélecteur CSS $selecteur ne correspond pas à $nombre éléments dans le noeud DOM $constate possédant la valeur $contenu.
$nombre peut avoir l'un des types suivants :
booléen: présuppose la présence correspondant au sélecteur (TRUE) ou l'absence d'éléments (FALSE).nombre entier: présuppose le nombre d'éléments.tableau: présuppose que le nombre est dans une plage indiquée en utilisant <, >, <= et >= comme clefs.Exemple 4.45. Utilisation de assertSelectEquals()
<?php
class SelectEqualsTest extends PHPUnit_Framework_TestCase
{
protected function setUp()
{
$this->xml = new DomDocument;
$this->xml->loadXML('<foo><bar>Baz</bar><bar>Baz</bar></foo>');
}
public function testAbsenceEchec()
{
$this->assertSelectEquals('foo bar', 'Baz', FALSE, $this->xml);
}
public function testPresenceEchec()
{
$this->assertSelectEquals('foo bar', 'Bat', TRUE, $this->xml);
}
public function testCompteExactEchec()
{
$this->assertSelectEquals('foo bar', 'Baz', 5, $this->xml);
}
public function testPlageEchec()
{
$this->assertSelectEquals('foo bar', 'Baz', array('>'=>6, '<'=>8), $this->xml);
}
}
?>
phpunit SelectEqualsTest
PHPUnit 3.6.0 by Sebastian Bergmann.
FFFF
Time: 0 seconds, Memory: 5.50Mb
There were 4 failures:
1) SelectEqualsTest::testAbsenceEchec
Failed asserting that true is false.
/home/sb/SelectEqualsTest.php:12
2) SelectEqualsTest::testPresenceEchec
Failed asserting that false is true.
/home/sb/SelectEqualsTest.php:17
3) SelectEqualsTest::testCompteExactEchec
Failed asserting that 2 matches expected 5.
/home/sb/SelectEqualsTest.php:22
4) SelectEqualsTest::testPlageEchec
Failed asserting that false is true.
/home/sb/SelectEqualsTest.php:27
FAILURES!
Tests: 4, Assertions: 4, Failures: 4.assertSelectRegExp(array $selecteur, string $motif, integer $nombre, mixed $constate[, string $message = '', boolean $isHtml = TRUE])
Rapporte une erreur identifiée par le $message si le sélecteur CSS $selecteur ne correspond pas à $nombre éléments dans le noeud DOM $constate possédant une valeur qui correspond au $motif.
$nombre peut avoir l'un des types suivants :
booléen: présuppose la présence d'éléments correspondant au sélecteur (TRUE) ou l'absence d'éléments (FALSE).nombre entier: présuppose le nombre d'éléments.tableau: présuppose que le nombre est dans une plage indiquée en utilisant <, >, <= et >= comme clefs.Exemple 4.46. Utilisation de assertSelectRegExp()
<?php
class SelectRegExpTest extends PHPUnit_Framework_TestCase
{
protected function setUp()
{
$this->xml = new DomDocument;
$this->xml->loadXML('<foo><bar>Baz</bar><bar>Baz</bar></foo>');
}
public function testAbsenceEchec()
{
$this->assertSelectRegExp('foo bar', '/Ba.*/', FALSE, $this->xml);
}
public function testPresenceEchec()
{
$this->assertSelectRegExp('foo bar', '/B[oe]z]/', TRUE, $this->xml);
}
public function testCompteExactEchec()
{
$this->assertSelectRegExp('foo bar', '/Ba.*/', 5, $this->xml);
}
public function testPlageEchec()
{
$this->assertSelectRegExp('foo bar', '/Ba.*/', array('>'=>6, '<'=>8), $this->xml);
}
}
?>
phpunit SelectRegExpTest
PHPUnit 3.6.0 by Sebastian Bergmann.
FFFF
Time: 0 seconds, Memory: 5.50Mb
There were 4 failures:
1) SelectRegExpTest::testAbsenceEchec
Failed asserting that true is false.
/home/sb/SelectRegExpTest.php:12
2) SelectRegExpTest::testPresenceEchec
Failed asserting that false is true.
/home/sb/SelectRegExpTest.php:17
3) SelectRegExpTest::testCompteExactEchec
Failed asserting that 2 matches expected 5.
/home/sb/SelectRegExpTest.php:22
4) SelectRegExpTest::testPlageEchec
Failed asserting that false is true.
/home/sb/SelectRegExpTest.php:27
FAILURES!
Tests: 4, Assertions: 4, Failures: 4.assertStringEndsWith(string $suffixe, string $chaine[, string $message = ''])
Rapporte une erreur identifiée par le $message si la $chaine ne se termine pas par $suffixe.
assertStringEndsNotWith() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.47. Utilisation de assertStringEndsWith()
<?php
class StringEndsWithTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertStringEndsWith('suffixe', 'foo');
}
}
?>
phpunit StringEndsWithTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 1 second, Memory: 5.00Mb
There was 1 failure:
1) StringEndsWithTest::testEchec
Failed asserting that 'foo' ends with "suffixe".
/home/sb/StringEndsWithTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertStringEqualsFile(string $fichierAttendu, string $chaineConstatee[, string $message = ''])
Rapporte une erreur identifiée par le $message si le fichier indiqué par $fichierAttendu ne possède pas $chaineConstatee comme contenu.
assertStringNotEqualsFile() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.48. Utilisation de assertStringEqualsFile()
<?php
class StringEqualsFileTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertStringEqualsFile('/home/sb/attendu', 'constate');
}
}
?>
phpunit StringEqualsFileTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) StringEqualsFileTest::testEchec
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'attendu
-'
+'constate'
/home/sb/StringEqualsFileTest.php:6
FAILURES!
Tests: 1, Assertions: 2, Failures: 1.assertStringStartsWith(string $prefixe, string $chaine[, string $message = ''])
Rapporte une erreur identifiée par le $message si la chaîne $chaine ne commence pas par $prefixe.
assertStringStartsNotWith() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.49. Utilisation de assertStringStartsWith()
<?php
class StringStartsWithTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertStringStartsWith('prefixe', 'foo');
}
}
?>
phpunit StringStartsWithTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) StringStartsWithTest::testEchec
Failed asserting that 'foo' starts with "prefixe".
/home/sb/StringStartsWithTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertTag(array $matcheur, string $constate[, string $message = '', boolean $isHtml = TRUE])
Rapporte une erreur identifiée par le $message si $constate n'établit pas de correspondance avec le $matcheur.
$matcheur est un tableau associatif qui indique les critères de correspondance pour l'assertion:
id: le noeud ayant l'attribut donné id doit correspondre à la valeur indiquée.tags: le type du noeud doit correspondre à la valeur correspondante.attributes: Les attributs du noeud doivent correspondre aux valeurs correspondantes dans le tableau associatif attributes.content: le contenu du texte doit correspondre à la valeur donnée.parent: le père du noeud doit correspondre au tableau associatif parent.child: au moins un des fils directs du noeud doit satisfaire aux critères décrits dans le tableau associatif child.ancestor: au moins l'un des ancêtres du noeud doit satisfaire aux critères décrits par le tableau associatif ancestor.descendant: au moins l'un des descendants du noeud doit satisfaire les critères décrits dans le tableau associatif descendant.children: tableau associatif pour compter les enfants d'un noeud.
count: le nombre d'enfants correspondants doit être égal à ce nombre.less_than: le nombre d'enfants correspondants doit être inférieur à ce nombre.greater_than: le nombre d'enfants correspondants doit être supérieur à ce nombre.only: un autre tableau associatif constitué de clefs à utiliser pour faire des correspondances avec les enfants, et seuls les enfants correspondants seront comptabilisés.assertNotTag() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.50. Utilisation de assertTag()
<?php
// Matcher qui présuppose qu'il existe un élément avec un id="mon_id".
$matcher = array('id' => 'mon_id');
// Matcher qui présuppose qu'il existe un tag "span".
$matcher = array('tag' => 'span');
// Matcher qui présuppose qu'il existe un tag "span" contenant
// "Hello World".
$matcher = array('tag' => 'span', 'content' => 'Hello World');
// Matcher qui présuppose qu'il existe un tag "span" dont le contenu correspond au
// motif d'expression rationnelle
$matcher = array('tag' => 'span', 'content' => '/Essayez P(HP|erl)/');
// Matcher qui présuppose qu'il existe un "span"avec un attribut class class.
$matcher = array(
'tag' => 'span',
'attributes' => array('class' => 'list')
);
// Matcher qui présuppose qu'il existe un "span" à l'intérieur d'un "div".
$matcher = array(
'tag' => 'span',
'parent' => array('tag' => 'div')
);
// Matcher qui présuppose qu'il existe un "span" quelque part dans une "table".
$matcher = array(
'tag' => 'span',
'ancestor' => array('tag' => 'table')
);
// Matcher qui présuppose qu'il existe un "span" avec au moins un fils "em".
$matcher = array(
'tag' => 'span',
'child' => array('tag' => 'em')
);
// Matcher qui présuppose qu'il existe un "span" contenant un tag "strong"
// (éventuellement imbriqué)
$matcher = array(
'tag' => 'span',
'descendant' => array('tag' => 'strong')
);
// Matcher qui présuppose qu'il existe un "span" contenant de 5 à 10 tags "em" comme
// fils directs
$matcher = array(
'tag' => 'span',
'children' => array(
'less_than' => 11,
'greater_than' => 4,
'only' => array('tag' => 'em')
)
);
// Matcher qui présuppose qu'il existe un "div", avec un ancêtre "ul" et un "li"
// parent (avec class="enum"), et contenant un descendant "span" qui contient
// un élément avec id="mon_test" et le texte "Hello World".
$matcher = array(
'tag' => 'div',
'ancestor' => array('tag' => 'ul'),
'parent' => array(
'tag' => 'li',
'attributes' => array('class' => 'enum')
),
'descendant' => array(
'tag' => 'span',
'child' => array(
'id' => 'mon_test',
'content' => 'Hello World'
)
)
);
// Utilise assertTag() pour appliquer un $matcher à un morceau de $html.
$this->assertTag($matcher, $html);
// Utilise assertTag() pour appliquer un matcher à un morceau de $xml.
$this->assertTag($matcher, $xml, '', FALSE);
?>
Des assertions plus complexes peuvent être formulées en utilisant les classes
PHPUnit_Framework_Constraint. Elles peuvent être évaluées
en utilisant la méthode assertThat().
Exemple 4.51, « Utilisation de assertThat() » montre comment
les contraintes logicalNot() et equalTo()
peuvent être utilisées pour exprimer la même assertion que
assertNotEquals().
assertThat(mixed $valeur, PHPUnit_Framework_Constraint $contrainte[, $message = ''])
Rapporte une erreur identifiée par le $message si la valeur $valeur ne correspond
pas à la $contrainte.
Exemple 4.51. Utilisation de assertThat()
<?php
class BiscuitTest extends PHPUnit_Framework_TestCase
{
public function testEquals()
{
$leBiscuit = new Biscuit('Ginger');
$monBiscuit = new Biscuit('Ginger');
$this->assertThat(
$leBiscuit,
$this->logicalNot(
$this->equalTo($monBiscuit)
)
);
}
}
?>
Tableau 4.3, « Contraintes » montre les
classes PHPUnit_Framework_Constraint disponibles.
Tableau 4.3. Contraintes
| Contrainte | Signification |
|---|---|
PHPUnit_Framework_Constraint_Attribute attribute(PHPUnit_Framework_Constraint $contrainte, $nomAttribut) | Contrainte qui applique une autre contrainte à l'attribut d'une classe ou d'un objet. |
PHPUnit_Framework_Constraint_IsAnything anything() | Contrainte qui accepte n'importe quelle valeur en entrée. |
PHPUnit_Framework_Constraint_ArrayHasKey arrayHasKey(mixed $clef) | Contrainte qui présuppose que le tableau pour lequel elle est évaluée possède une clef donnée.. |
PHPUnit_Framework_Constraint_TraversableContains contains(mixed $valeur) | Contrainte qui présuppose que le tableau ou l'objet qui implémente l'interface Iterator pour lequel elle est évaluée contient une valeur donnée.. |
PHPUnit_Framework_Constraint_IsEqual equalTo($valeur, $delta = 0, $profondeurMax = 10) | Contrainte qui vérifie si une valeur est égale à une autre. |
PHPUnit_Framework_Constraint_Attribute attributeEqualTo($nomAttribut, $valeur, $delta = 0, $profondeurMax = 10) | Contrainte qui vérifie si une valeur est égale à l'attribut d'une classe ou d'un objet. |
PHPUnit_Framework_Constraint_FileExists fileExists() | Contrainte qui vérifie si le (nom de) fichier pour lequel elle est évaluée existe. |
PHPUnit_Framework_Constraint_GreaterThan greaterThan(mixed $valeur) | Contrainte qui présuppose que la valeur pour laquelle elle est évaluée est supérieure à une valeur donnée. |
PHPUnit_Framework_Constraint_Or greaterThanOrEqual(mixed $valeur) | Contrainte qui présuppose que la valeur pour laquelle elle est évaluée et supérieure ou égale à une valeur donnée. |
PHPUnit_Framework_Constraint_ClassHasAttribute classHasAttribute(string $nomAttribut) | Contrainte qui présuppose que la classe pour laquelle elle est évaluée possède un attribut donné. |
PHPUnit_Framework_Constraint_ClassHasStaticAttribute classHasStaticAttribute(string $nomAttribut) | Contrainte qui présuppose que la classe pour laquelle elle est évaluée possède un attribut statique donné. |
PHPUnit_Framework_Constraint_ObjectHasAttribute hasAttribute(string $nomAttribut) | Contrainte qui présuppose que l'objet pour lequel elle est évaluée possède un attribut donné. |
PHPUnit_Framework_Constraint_IsIdentical identicalTo(mixed $valeur) | Contrainte qui présuppose qu'une valeur est identique à une autre. |
PHPUnit_Framework_Constraint_IsFalse isFalse() | Contrainte qui présuppose que la valeur pour laquelle elle est évaluée est FALSE. |
PHPUnit_Framework_Constraint_IsInstanceOf isInstanceOf(string $nomClasse) | Contrainte qui présuppose que l'objet pour lequel elle est évaluée est une instance d'une classe donnée. |
PHPUnit_Framework_Constraint_IsNull isNull() | Contrainte qui présuppose que la valeur pour laquelle elle est évaluée est NULL. |
PHPUnit_Framework_Constraint_IsTrue isTrue() | Contrainte qui présuppose que la valeur pour laquelle elle est évaluée est TRUE. |
PHPUnit_Framework_Constraint_IsType isType(string $type) | Contrainte qui présuppose que la valeur pour laquelle elle est évaluée est du type indiqué. |
PHPUnit_Framework_Constraint_LessThan lessThan(mixed $valeur) | Contrainte qui présuppose que la valeur pour laquelle elle est évaluée est inférieure à la valeur donnée. |
PHPUnit_Framework_Constraint_Or lessThanOrEqual(mixed $valeur) | Contrainte qui présuppose que la valeur pour laquelle elle est évaluée est inférieure ou égale à une valeur donnée. |
logicalAnd() | ET logique (AND). |
logicalNot(PHPUnit_Framework_Constraint $contrainte) | NON logique (NOT). |
logicalOr() | OU logique (OU). |
logicalXor() | OU exclusif logique (XOR). |
PHPUnit_Framework_Constraint_PCREMatch matchesRegularExpression(string $motif) | Contrainte qui présuppose que la chaîne pour laquelle elle est évaluée correspond à une expression rationnelle. |
PHPUnit_Framework_Constraint_StringContains stringContains(string $chaine, bool $casse) | Contrainte qui présuppose que la chaîne pour laquelle elle est évaluée contient une chaîne donnée. |
PHPUnit_Framework_Constraint_StringEndsWith stringEndsWith(string $suffixe) | Contrainte qui présuppose que la chaîne pour laquelle elle est évaluée se termine avec un suffixe donné. |
PHPUnit_Framework_Constraint_StringStartsWith stringStartsWith(string $prefixe) | Contrainte qui présuppose que la chaîne pour laquelle elle est évaluée commence par un préfixe donné. |
assertTrue(bool $condition[, string $message = ''])
Rapporte une erreur identifiée par le $message si la $condition est FALSE.
Exemple 4.52. Utilisation de assertTrue()
<?php
class TrueTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertTrue(FALSE);
}
}
?>
phpunit TrueTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) TrueTest::testEchec
Failed asserting that false is true.
/home/sb/TrueTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertXmlFileEqualsXmlFile(string $fichierAttendu, string $fichierConstate[, string $message = ''])
Rapporte une erreur identifiée par le $message si le document XML dans $fichierConstate n'est pas égal au document XML dans $fichierAttendu.
assertXmlFileNotEqualsXmlFile() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.53. Utilisation de assertXmlFileEqualsXmlFile()
<?php
class XmlFileEqualsXmlFileTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertXmlFileEqualsXmlFile(
'/home/sb/attendu.xml', '/home/sb/constate.xml');
}
}
?>
phpunit XmlFileEqualsXmlFileTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) XmlFileEqualsXmlFileTest::testEchec
Failed asserting that two DOM documents are equal.
--- Expected
+++ Actual
@@ @@
<?xml version="1.0"?>
<foo>
- <bar/>
+ <baz/>
</foo>
/home/sb/XmlFileEqualsXmlFileTest.php:7
FAILURES!
Tests: 1, Assertions: 3, Failures: 1.assertXmlStringEqualsXmlFile(string $fichierAttendu, string $xmlConstate[, string $message = ''])
Rapporte une erreur identifiée par le $message si le document XML dans $xmlConstate n'est pas égal au document XML dans $fichierAttendu.
assertXmlStringNotEqualsXmlFile() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.54. Utilisation de assertXmlStringEqualsXmlFile()
<?php
class XmlStringEqualsXmlFileTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertXmlStringEqualsXmlFile(
'/home/sb/attendu.xml', '<foo><baz/></foo>');
}
}
?>
phpunit XmlStringEqualsXmlFileTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) XmlStringEqualsXmlFileTest::testEchec
Failed asserting that two DOM documents are equal.
--- Expected
+++ Actual
@@ @@
<?xml version="1.0"?>
<foo>
- <bar/>
+ <baz/>
</foo>
/home/sb/XmlStringEqualsXmlFileTest.php:7
FAILURES!
Tests: 1, Assertions: 2, Failures: 1.assertXmlStringEqualsXmlString(string $xmlAttendu, string $xmlConstateXml[, string $message = ''])
Rapporte une erreur identifiée par le $message si le document XML dans $xmlConstate n'est pas égal au document XML dans $xmlAttendu.
assertXmlStringNotEqualsXmlString() est l'inverse de cette assertion et prend les mêmes arguments.
Exemple 4.55. Utilisation de assertXmlStringEqualsXmlString()
<?php
class XmlStringEqualsXmlStringTest extends PHPUnit_Framework_TestCase
{
public function testEchec()
{
$this->assertXmlStringEqualsXmlString(
'<foo><bar/></foo>', '<foo><baz/></foo>');
}
}
?>
phpunit XmlStringEqualsXmlStringTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) XmlStringEqualsXmlStringTest::testEchec
Failed asserting that two DOM documents are equal.
--- Expected
+++ Actual
@@ @@
<?xml version="1.0"?>
<foo>
- <bar/>
+ <baz/>
</foo>
/home/sb/XmlStringEqualsXmlStringTest.php:7
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
Le lanceur de tests en ligne de commandes de PHPUnit peut être appelé via
la commande phpunit. Le code suivant montre comment exécuter
des tests avec le lanceur de tests en ligne de commandes de PHPUnit:
phpunit ArrayTest
PHPUnit 3.6.0 by Sebastian Bergmann.
..
Time: 0 seconds
OK (2 tests, 2 assertions)Pour chaque test exécuté, l'outil en ligne de commandes de PHPUnit affiche un caractère pour indiquer l'avancement:
.Affiché quand le test a réussi.
FAffiché quand une assertion échoue lors de l'exécution d'une méthode de test.
EAffiché quand une erreur survient pendant l'exécution d'une méthode de test.
SAffiché quand le test a été sauté (voir Chapitre 9, Tests incomplets et sautés).
IAffiché quand le test est marqué comme incomplet ou pas encore implémenté (voir Chapitre 9, Tests incomplets et sautés).
PHPUnit différencie les échecs et les
erreurs. Un échec est une assertion PHPUnit violée
comme un appel en échec de assertEquals().
Une erreur est une exception inattendue ou une erreur PHP. Parfois
cette distinction s'avère utile car les erreurs tendent à être plus faciles
à corriger que les échecs. Si vous avez une longue liste de problèmes, il vaut
mieux éradiquer d'abord les erreurs pour voir s'il reste encore des échecs
uen fois qu'elles ont été corrigées.
Jetons un oeil aux options du lanceur de tests en ligne de commandes dans le code suivant :
phpunit --help
PHPUnit 3.6.0 by Sebastian Bergmann.
Usage: phpunit [switches] UnitTest [UnitTest.php]
phpunit [switches] <directory>
--log-junit <file> Log test execution in JUnit XML format to file.
--log-tap <file> Log test execution in TAP format to file.
--log-json <file> Log test execution in JSON format.
--coverage-clover <file> Generate code coverage report in Clover XML format.
--coverage-html <dir> Generate code coverage report in HTML format.
--coverage-php <file> Serialize PHP_CodeCoverage object to file.
--coverage-text=<file> Generate code coverage report in text format.
Default to writing to the standard output.
--testdox-html <file> Write agile documentation in HTML format to file.
--testdox-text <file> Write agile documentation in Text format to file.
--filter <pattern> Filter which tests to run.
--group ... Only runs tests from the specified group(s).
--exclude-group ... Exclude tests from the specified group(s).
--list-groups List available test groups.
--loader <loader> TestSuiteLoader implementation to use.
--printer <printer> TestSuiteListener implementation to use.
--repeat <times> Runs the test(s) repeatedly.
--tap Report test execution progress in TAP format.
--testdox Report test execution progress in TestDox format.
--colors Use colors in output.
--stderr Write to STDERR instead of STDOUT.
--stop-on-error Stop execution upon first error.
--stop-on-failure Stop execution upon first error or failure.
--stop-on-skipped Stop execution upon first skipped test.
--stop-on-incomplete Stop execution upon first incomplete test.
--strict Run tests in strict mode.
-v|--verbose Output more verbose information.
--debug Display debbuging information during test execution.
--process-isolation Run each test in a separate PHP process.
--no-globals-backup Do not backup and restore $GLOBALS for each test.
--static-backup Backup and restore static attributes for each test.
--bootstrap <file> A "bootstrap" PHP file that is run before the tests.
-c|--configuration <file> Read configuration from XML file.
--no-configuration Ignore default configuration file (phpunit.xml).
--include-path <path(s)> Prepend PHP's include_path with given path(s).
-d key[=value] Sets a php.ini value.
-h|--help Prints this usage information.
--version Prints the version and exits.
--debug Output debugging information.phpunit UnitTest
Exécute les tests qui sont fournis par la classe
UnitTest. Cette classe est supposée être déclarée
dans le fichier source UnitTest.php.
UnitTest doit soit être une classe qui hérite
de PHPUnit_Framework_TestCase soit une classe qui
fournit une méthode public static suite() retournant
un objet PHPUnit_Framework_Test, par exemple
une instance de la classe
PHPUnit_Framework_TestSuite.
phpunit UnitTest UnitTest.php
Exécute les tests qui sont fournis par la classe
UnitTest. Cette classe est supposée être déclarée
dans le fichier source indiqué.
--log-junitGénère un fichier de log au format JUnit XML pour les tests exécutés. Voir Chapitre 18, Journalisation pour plus de détails.
--log-tapGénère un fichier de log utilisant le format Test Anything Protocol (Protocol de test universel ou TAP) pour les tests exécutés. Voir Chapitre 18, Journalisation pour plus de détails.
--log-jsonGénère un fichier de log en utilisant le format JSON. Voir Chapitre 18, Journalisation pour plus de détails.
--coverage-htmlGénère un rapport de couverture de code au format HTML. Voir Chapitre 14, Analyse de couverture de code pour plus de détails.
Merci de noter que cette fonctionnalité n'est seulement disponible que lorsque les extensions tokenizer et Xdebug sont installées.
--coverage-cloverGénère un fichier de log au format XML avec les informations de couverture de code pour les tests exécutés. Voir Chapitre 18, Journalisation pour plus de détails.
Merci de noter que cette fonctionnalité n'est seulement disponible que lorsque les extensions tokenizer et Xdebug sont installées.
--coverage-phpGénère un objet sérialisé PHP_CodeCoverage contenant les informations de couverture de code.
Merci de noter que cette fonctionnalité n'est seulement disponible que lorsque les extensions tokenizer et Xdebug sont installées.
--coverage-textGénère un fichier de log ou une sortie écran sur la ligne de commandes en format humainement lisible avec les informations de couverture de code pour les tests exécutés. Voir Chapitre 18, Journalisation pour plus de détails.
Merci de noter que cette fonctionnalité n'est seulement disponible que lorsque les extensions tokenizer et Xdebug sont installées.
--testdox-html et --testdox-textGénère la documentation agile au format HTML ou texte pur pour les tests exécutés. Voir Chapitre 15, Autres utilisations des tests pour plus de détails.
--filterExécute seulement les tests dont le nom correspond au motif donné. Le motif peut être soit le nom d'un test particulier, soit une expression rationnelle qui correspond à plusieurs noms de tests.
--group
Exécute seulement les tests appartenant à un/des groupe(s) indiqué(s). Un test
peut être signalé comme appartenant à un groupe en utilisant l'annotation
@group.
L'annotation @author est un alias pour
@group permettant de filtrer les tests en se basant
sur leurs auteurs.
--exclude-group
Exclut les tests d'un/des groupe(s) indiqué(s). Un test peut être signalé
comme appartenant à un groupe en utilisant l'annotation @group.
--list-groupsListe les groupes de tests disponibles.
--loader
Indique l'implémentation de PHPUnit_Runner_TestSuiteLoader
à utiliser.
Le chargeur standard de suite de tests va chercher les fichiers source
dans le répertoire de travail actuel et dans chaque répertoire qui
est indiqué dans la directive de configuration PHP include_path.
Suivant les conventions de nommage PEAR, le nom d'une classe tel que
Projet_Paquetage_Classe est calqué sur le nom de fichier source
Projet/Paquetage/Classe.php.
--printer
Indique l'afficheur de résultats à utiliser. Cette classe d'afficheur doit
hériter de PHPUnit_Util_Printer et implémenter l'interface
PHPUnit_Framework_TestListener.
--repeatRépéter l'exécution du(des) test(s) le nombre indiqué de fois.
--tapRapporte l'avancement des tests en utilisant le Test Anything Protocol (TAP). Voir Chapitre 18, Journalisation pour plus de détails.
--testdoxRapporte l'avancement des tests sous forme de documentation agile. Voir Chapitre 15, Autres utilisations des tests pour plus de détails.
--colorsUtilise des couleurs pour l'affichage.
--stderr
Utilise optionnellement STDERR au lieu de
STDOUT pour l'affichage.
--stop-on-errorArrête l'exécution à la première erreur.
--stop-on-failureArrête l'exécution à la première erreur ou au premier échec.
--stop-on-skippedArrête l'exécution au premier test sauté.
--stop-on-incompleteArrête l'exécution au premier test incomplet.
--strictExécute les tests en mode strict.
--verboseAffiche des informations plus détaillées, par exemple le nom des tests qui sont incomplets ou qui ont été sautés.
--process-isolationExécute chaque test dans un processus PHP distinct.
--no-globals-backupNe pas sauvegarder et restaurer $GLOBALS. Voir la section intitulée « Etat global » pour plus de détails.
--static-backupSauvegarde et restaure les attributs statiques des classes définies par l'utilisateur. Voir la section intitulée « Etat global » pour plus de détails.
--bootstrapUn fichier PHP "amorce" ("bootstrap") est exécuté avant les tests.
--configuration, -cLit la configuration dans un fichier XML. Voir Annexe C, Le fichier de configuration Configuration pour plus de détails.
Si phpunit.xml ou
phpunit.xml.dist (dans cet ordre) existent dans le
répertoire de travail actuel et que --configuration n'est
pas utilisé, la configuration sera automatiquement
lue dans ce fichier.
--no-configuration
Ignorer phpunit.xml et
phpunit.xml.dist du répertoire de travail actuel.
--include-path
Préfixe l'include_path PHP avec le(s) chemin(s) donné(s).
-dFixe la valeur des options de configuration PHP données.
--debugAffiche des informations de débogage telles que le nom d'un test quand son exécution démarre.
L'une des parties les plus consommatrices en temps lors de l'écriture de tests est d'écrire le code pour configurer le monde dans un état connu puis de le remettre dans son état initial quand le test est terminé. Cet état connu est appelé la fixture du test.
Dans Exemple 4.1, « Tester des opérations de tableau avec PHPUnit », la
fixture était simplement le tableau sauvegardé dans la variable $fixture.
La plupart du temps, cependant, la fixture sera beaucoup plus complexe
qu'un simple tableau, et le volume de code nécessaire pour la mettre en place
croîtra dans les mêmes proportions. Le contenu effectif du test sera perdu
dans le bruit de configuration de la fixture. Ce problème s'aggrave quand
vous écrivez plusieurs tests doté de fixtures similaires. Sans l'aide du
framework de test, nous aurions à dupliquer le code qui configure la fixture
pour chaque test que nous écrivons.
PHPUnit gère le partage du code de configuration. Avant qu'une méthode de test ne soit
lancée, une méthode canevas appelée setUp() est invoquée.
setUp() est l'endroit où vous créez les objets sur lesquels
vous allez passer les tests. Une fois que la méthode de test est finie, qu'elle ait
réussi ou échoué, une autre méthode canevas appelée
tearDown() est invoquée. tearDown()
est l'endroit où vous nettoyez les objets sur lesquels vous avez passé les tests.
Dans Exemple 4.2, « Utiliser l'annotation @depends pour exprimer des dépendances » nous avons
utilisé la relation producteur-consommateur entre les tests pour partager une fixture. Ce
n'est pas toujours souhaitable ni même possible. Exemple 6.1, « Using setUp() to create the stack fixture »
montre comme nous pouvons écrire les tests de PileTest de telle façon
que ce n'est pas la fixture elle-même qui est réutilisée mais le code qui l'a créée.
D'abord nous déclarons la variable d'instance, $pile, que nous
allons utiliser à la place d'une variable locale à la méthode. Puis nous plaçons
la création de la fixture tableau dans la méthode
setUp(). Enfin, nous supprimons le code redondant des méthodes
de test et nous utilisons la variable d'instance nouvellement introduite.
$this->pile, à la place de la variable locale à la méthode
$pile avec la méthode d'assertion assertEquals().
Exemple 6.1. Using setUp() to create the stack fixture
<?php
class PileTest extends PHPUnit_Framework_TestCase
{
protected $pile;
protected function setUp()
{
$this->pile = array();
}
public function testVide()
{
$this->assertTrue(empty($this->pile));
}
public function testPush()
{
array_push($this->pile, 'foo');
$this->assertEquals('foo', $this->pile[count($this->pile)-1]);
$this->assertFalse(empty($this->pile));
}
public function testPop()
{
array_push($this->pile, 'foo');
$this->assertEquals('foo', array_pop($this->pile));
$this->assertTrue(empty($this->pile));
}
}
?>
Les méthodes canevas setUp() et tearDown()
sont exécutées une fois pour chaque méthode de test (et pour les nouvelles instances)
de la classe de cas de test.
De plus, les méthodes canevas setUpBeforeClass() et
tearDownAfterClass() sont appelées respectivement avant
que le premier test de la classe de cas de test ne soit exécuté et après
que le dernier test de la classe de test a été exécuté.
L'exemple ci-dessous montre toutes les méthodes canevas qui sont disponibles dans une classe de cas de test.
Exemple 6.2. Exemple montrant toutes les méthodes canevas disponibles
<?php
class TemplateMethodsTest extends PHPUnit_Framework_TestCase
{
public static function setUpBeforeClass()
{
fwrite(STDOUT, __METHOD__ . "\n");
}
protected function setUp()
{
fwrite(STDOUT, __METHOD__ . "\n");
}
protected function assertPreConditions()
{
fwrite(STDOUT, __METHOD__ . "\n");
}
public function testOne()
{
fwrite(STDOUT, __METHOD__ . "\n");
$this->assertTrue(TRUE);
}
public function testTwo()
{
fwrite(STDOUT, __METHOD__ . "\n");
$this->assertTrue(FALSE);
}
protected function assertPostConditions()
{
fwrite(STDOUT, __METHOD__ . "\n");
}
protected function tearDown()
{
fwrite(STDOUT, __METHOD__ . "\n");
}
public static function tearDownAfterClass()
{
fwrite(STDOUT, __METHOD__ . "\n");
}
protected function onNotSuccessfulTest(Exception $e)
{
fwrite(STDOUT, __METHOD__ . "\n");
throw $e;
}
}
?>
phpunit TemplateMethodsTest
PHPUnit 3.6.0 by Sebastian Bergmann.
TemplateMethodsTest::setUpBeforeClass
TemplateMethodsTest::setUp
TemplateMethodsTest::assertPreConditions
TemplateMethodsTest::testOne
TemplateMethodsTest::assertPostConditions
TemplateMethodsTest::tearDown
.TemplateMethodsTest::setUp
TemplateMethodsTest::assertPreConditions
TemplateMethodsTest::testTwo
TemplateMethodsTest::tearDown
TemplateMethodsTest::onNotSuccessfulTest
FTemplateMethodsTest::tearDownAfterClass
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) TemplateMethodsTest::testTwo
Failed asserting that <boolean:false> is true.
/home/sb/TemplateMethodsTest.php:30
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
setUp() et tearDown() sont sympathiquement
symétriques en théorie mais pas en pratique. En pratique, vous n'avez besoin
d'implémenter tearDown() que si vous avez alloué
des ressources externes telles que des fichiers ou des sockets dans
setUp(). Si votre setUp() ne crée simplement
que de purs objets PHP, vous pouvez généralement ignorer tearDown().
Cependant, si vous créez de nombreux objets dans votre setUp(), vous
pourriez vouloir libérer (unset()) les variables pointant vers
ces objets dans votre tearDown() de façon à ce qu'ils puissent être
récupérés par le ramasse-miettes. Le nettoyage des objets de cas de test n'est pas
prévisible.
Que se passe-t'il si vous avez deux tests avec deux setups légèrement différents ? Il y a deux possibilités :
Si le code des setUp() ne diffère que légèrement, extrayez le
code qui diffère du code de setUp() pour le mettre dans la méthode
de test.
Si vous avez vraiment deux setUp() différentes, vous
avez besoin de classes de cas de test différentes. Nommez les classes selon
les différences constatées dans les setup.
Il existe quelques bonnes raisons pour partager des fixtures entre les tests, mais dans la plupart des cas la nécessité de partager une fixture entre plusieurs tests résulte d'un problème de conception non résolu.
Un bon exemple de fixture qu'il est raisonnable de partager entre plusieurs tests est une connexion à une base de données : vous vous connectez une fois à la base de données et vous réutilisez cette connexion au lieu d'en créer une nouvelle pour chaque test. Ceci rend vos tests plus rapides.
Exemple 6.3, « Partager les fixtures entre les tests d'une série de tests »
utilise les méthodes canevas setUpBeforeClass() et
tearDownAfterClass() pour respectivement établir la connexion à la
base de données avant le premier test de la classe de cas de test et pour
de déconnecter de la base de données après le dernier test du cas de test.
Exemple 6.3. Partager les fixtures entre les tests d'une série de tests
<?php
class DatabaseTest extends PHPUnit_Framework_TestCase
{
protected static $dbh;
public static function setUpBeforeClass()
{
self::$dbh = new PDO('sqlite::memory:');
}
public static function tearDownAfterClass()
{
self::$dbh = NULL;
}
}
?>
On n'insistera jamais assez sur le fait que partager les fixtures entre les tests réduit la valeur de ces tests. Le problème de conception sous-jacent est que les objets ne sont pas faiblement couplés. Vous pourrez obtenir de meilleurs résultats en résolvant le problème de conception sous-jacent puis en écrivant des tests utilisant des bouchons (voir Chapitre 10, Doublure de test), plutôt qu'en créant des dépendances entre les tests à l'exécution et en ignorant l'opportunité d'améliorer votre conception.
Il est difficile de tester du code qui utilise des singletons. La même chose est vraie pour le code qui utilise des variables globales. Typiquement, le code que vous voulez tester est fortement couplé avec une variable globale et vous ne pouvez pas contrôler sa création. Un problème additionnel réside dans le fait qu'un test qui modifie une variable globale peut faire échouer un autre test.
En PHP, les variables globales fonctionnent comme ceci :
Une variable globale $foo = 'bar'; est enregistrée comme $GLOBALS['foo'] = 'bar';.
La variable $GLOBALS est une variable appelée super-globale.
Les variables super-globales sont des variables internes qui sont toujours disponibles dans toutes les portées.
Dans la portée d'une fonction ou d'une méthode, vous pouvez accéder à la variable globale $foo soit en accédant directement à $GLOBALS['foo'] soit en utilisant global $foo; pour créer une variable locale faisant référence à la variable globale.
A part les variables globales, les attributs statiques des classes font également partie de l'état global.
Par défaut, PHPUnit exécute vos tests de façon à ce que des modifications
aux variables globales et super-globales (
$GLOBALS,
$_ENV, $_POST,
$_GET, $_COOKIE,
$_SERVER, $_FILES,
$_REQUEST) n'affectent pas les autres tests. Optionnellement,
cette indépendance peut être étendue aux attributs statiques des classes.
L'implémentation des opérations de sauvegarde et de restauration des attributs statiques des classes nécessite PHP 5.3 (ou supérieur).
L'implémentation des opérations de sauvegarde et de restauration des variables
globales et des attributs statiques des classes utilise
serialize() et unserialize().
Les objets de certaines classes qui sont fournis pas PHP lui-même, tel que
PDO par exemple, ne peuvent pas être sérialisés si bien que
l'opération de sauvegarde va échouer quand un tel objet sera enregistré dans le tableau
$GLOBALS, par exemple.
L'annotation @backupGlobals qui est discutée dans
la section intitulée « @backupGlobals » peut être utilisée pour
contrôler les opérations de sauvegarde et de restauration des variables globales.
Alternativement, vous pouvez fournir une liste noire des variables globales qui doivent
être exclues des opérations de sauvegarde et de restauration comme ceci :
class MonTest extends PHPUnit_Framework_TestCase
{
protected $backupGlobalsBlacklist = array('globalVariable');
// ...
}
Merci de noter que le réglage de l'attribut $backupGlobalsBlacklist
à l'intérieur de la méthode setUp(), par exemple, n'a aucun effet.
L'annotation @backupStaticAttributes qui est discutée dans
la section intitulée « @backupStaticAttributes » peut être utilisée pour
contrôler les opérations de sauvegarde et de restauration des attributs statiques.
Alternativement, vous pouvez fournir une liste noire des attributs statiques qui doivent
être exclus des opérations de sauvegarde et de restauration comme ceci :
class MonTest extends PHPUnit_Framework_TestCase
{
protected $backupStaticAttributesBlacklist = array(
'className' => array('attributeName')
);
// ...
}
Merci de noter que le réglage de l'attribut $backupStaticAttributesBlacklist
à l'intérieur de la méthode setUp(), par exemple, n'a aucun effet.
L'un des objectifs de PHPUnit (voir Chapitre 2, Objectifs de PHPUnit) est que les tests soient combinables: nous voulons pouvoir exécuter n'importe quel nombre ou combinaison de tests ensembles, par exemple tous les tests pour le projet entier, ou les tests pour toutes les classes d'un composant qui constitue une partie du projet ou simplement les tests d'une seule classe particulière.
PHPUnit gère différente façon d'organiser les tests et de les combiner en une suite de tests. Ce chapitre montre les approches les plus communément utilisées.
La façon probablement la plus simple de composer une suite de tests est de conserver tous les fichiers sources des cas de test dans un répertoire test. PHPUnit peut automatiquement trouver et exécuter les tests en parcourant récursivement le répertoire test.
Jetons un oeil à la suite de tests de la bibliothèque Object_Freezer. En regardant la structure des répertoires du projet, nous voyons que
les classes de cas de test dans le répertoire Tests reflètent la structure des
paquetages et des classes du système en cours de test (SCT, System Under Test ou SUT) dans le répertoire
Object:
Object Tests |-- Freezer |-- Freezer | |-- HashGenerator | |-- HashGenerator | | `-- NonRecursiveSHA1.php | | `-- NonRecursiveSHA1Test.php | |-- HashGenerator.php | | | |-- IdGenerator | |-- IdGenerator | | `-- UUID.php | | `-- UUIDTest.php | |-- IdGenerator.php | | | |-- LazyProxy.php | | | |-- Storage | |-- Storage | | `-- CouchDB.php | | `-- CouchDB | | | | |-- WithLazyLoadTest.php | | | | `-- WithoutLazyLoadTest.php | |-- Storage.php | |-- StorageTest.php | `-- Util.php | `-- UtilTest.php `-- Freezer.php `-- FreezerTest.php
Pour exécuter tous les tests de la bibliothèque, nous n'avons qu'à faire pointer le lanceur de tests en ligne de commandes de PHPUnit sur ce répertoire test :
phpunit Tests
PHPUnit 3.6.0 by Sebastian Bergmann.
............................................................ 60 / 75
...............
Time: 0 seconds
OK (75 tests, 164 assertions)
Si vous pointez le lanceur de tests en ligne de commandes de PHPUnit sur
un répertoire, il va chercher les fichiers
*Test.php.
Pour n'exécuter que les tests déclarés dans la classe de cas de test
Object_FreezerTest dans Tests/FreezerTest.php,
nous pouvons utiliser la commande suivante :
phpunit Tests/FreezerTest
PHPUnit 3.6.0 by Sebastian Bergmann.
............................
Time: 0 seconds
OK (28 tests, 60 assertions)
Pour un contrôle plus fin sur les tests à exécuter, nous pouvons utiliser
l'option --filter:
phpunit --filter testFreezingAnObjectWorks Tests
PHPUnit 3.6.0 by Sebastian Bergmann.
.
Time: 0 seconds
OK (1 test, 2 assertions)Un inconvénient de cette approche est que nous n'avons pas de contrôle sur l'ordre dans lequel les tests sont exécutés. Ceci peut conduire à des problèmes concernant les dépendances des tests, voir la section intitulée « Dépendances des tests ». Dans la prochaine section, nous verrons comment nous pouvons rendre l'ordre d'exécution des tests explicité en utilisant le fichier de configuration XML.
Le fichier de configuration XML de PHPUnit (Annexe C, Le fichier de configuration Configuration)
peut aussi être utilisé pour composer une suite de tests.
Exemple 7.1, « Composer une suite de tests en utilisant la configuration XML »
montre un exemple minimaliste qui va ajouter toutes les classes *Test
trouvées dans les fichiers *Test.php quand
Tests est parcouru récursivement.
Exemple 7.1. Composer une suite de tests en utilisant la configuration XML
<phpunit>
<testsuites>
<testsuite name="Object_Freezer">
<directory>Tests</directory>
</testsuite>
</testsuites>
</phpunit>L'ordre dans lequel les tests sont exécutés peut être rendu explicite :
Exemple 7.2. Composer une suite de tests en utilisant la configuration XML
<phpunit>
<testsuites>
<testsuite name="Object_Freezer">
<file>Tests/Freezer/HashGenerator/NonRecursiveSHA1Test.php</file>
<file>Tests/Freezer/IdGenerator/UUIDTest.php</file>
<file>Tests/Freezer/UtilTest.php</file>
<file>Tests/FreezerTest.php</file>
<file>Tests/Freezer/StorageTest.php</file>
<file>Tests/Freezer/Storage/CouchDB/WithLazyLoadTest.php</file>
<file>Tests/Freezer/Storage/CouchDB/WithoutLazyLoadTest.php</file>
</testsuite>
</testsuites>
</phpunit>De nombreux exemples de tests unitaires de niveau débutant ou intermédiaire dans de nombreux langages de programmation suggèrent qu'il est parfaitement facile de tester la logique de votre application avec de simples tests. Pour les applications centrées sur une base de données, c'est loin d'être la réalité. Commencez à utiliser WordPress, TYPO3 ou Symfony avec Doctrine ou Propel, par exemple, et nous serez vite confrontés à des problèmes considérables avec PHPUnit : juste parce que la base de données est vraiment étroitement liée à ces bibliothèques.
Vous connaissez probablement ce scénario rencontré tous les jours sur les projets, dans lequel vous voulez mettre à l'oeuvre votre savoir-faire tout neuf ou déjà aguerri en matière de PHPUnit et où vous vous retrouvez bloqué par l'un des problèmes suivants :
La méthode que vous voulez tester exécute une opération de jointure plutôt vaste et utilise les données pour calculer certains résultats importants.
Votre logique métier exécute un mélange d'instructions SELECT, INSERT, UPDATE et DELETE.
Vous devez configurer les données de test dans (éventuellement beaucoup) plus de deux tables pour obtenir des données initiales raisonnables pour les méthodes que vous voulez tester.
L'extension DbUnit simplifie considérablement la configuration d'une base de données à des fins de test et vous permet de vérifier le contenu d'une base de données après avoir réalisé une suite d'opérations. Elle peut être installée comme ceci :
pear install phpunit/DbUnitDbUnit gère actuellement MySQL, PostgreSQL, Oracle et SQLite. Via l'intégration de Zend Framework ou de Doctrine 2 il est possible d'accéder à d'autres systèmes de base de données comme IBM DB2 ou Microsoft SQL Server.
Il y a une bonne raison pour laquelle les exemples concernant le test unitaire n'inclut pas d'interaction avec une base de données : ces types de test sont à la fois complexes à configurer et à maintenir. Quand vous faites des tests sur votre base de données, vous devez prendre soin des variables suivantes :
Le schéma et les tables de la base de données
Insérer les lignes nécessaires pour le test dans ces tables
Vérifier l'état de la base de données après que votre test a été exécuté
Nettoyer la base de données pour chaque nouveau test
Comme de nombreuses APIs de base de données comme PDO, MySQLi ou OCI8 sont lourdes à utiliser et verbeuses à écrire, réaliser ces étapes à la main est un cauchemar absolu.
Le code de test doit être aussi court et précis que possible pour plusieurs raisons :
Vous ne voulez pas modifier un volume considérable de code de test pour de petites modifications dans votre code de production.
Vous voulez être capable de lire et de comprendre le code de test facilement, même des mois après l'avoir écrit.
De plus, vous devez prendre conscience que la base de données est essentiellement une variable globale en entrée pour votre code. Deux tests de votre série de tests peuvent être exécutés sur la même base de données, potentiellement en réutilisant les données plusieurs fois. Un échec dans un test peut facilement affecter le résultat des tests suivants rendant votre expérimentation de test très difficile. L'étape de nettoyage mentionnée précédemment est d'une importance majeure pour résoudre le problème posé par le fait que « la base de données est une variable globale en entrée ».
DbUnit aide à simplifier tous ces problèmes avec le test de base de données d'une manière élégante.
Là où PHPUnit ne peut pas vous aider c'est pour le fait que les tests de base de données sont très lents comparés aux tests n'en utilisant pas. Selon l'importance des interactions avec votre base de données, vos tests peuvent s'exécuter sur une durée considérable. Cependant, si vous gardez petit le volume de données utilisées pour chaque test et que vous essayez de tester le plus de code possible en utilisant des tests qui ne font pas appel à une base de données, vous pouvez facilement rester très en dessous d'une minute, même pour de grandes séries de tests.
La suite de test du projet Doctrine 2 , par exemple, possède actuellement une suite de tests d'environ 1000 tests dont presque la moitié accède à la base de données et continue à s'exécuter en 15 secondes sur une base de données MySQL sur un ordinateur de bureau standard.
Dans son livre sur les canevas de tests xUnit, Gerard Meszaros liste les quatre phases d'un test unitaire :
Configurer une fixture
Expérimenter le système à tester
Vérifier les résultats
Nettoyer
Qu'est-ce qu'une fixture ?
Une fixture décrit l'état initial dans lequel se trouvent votre application et votre base de données quand vous exécutez un test.
Tester la base de données nécessite au moins d'intervenir dans setup et teardown pour nettoyer et écrire les données de fixture nécessaires dans vos tables. Cependant, l'extension de base de données possède une bonne raison de ramener les quatre phases dans un test de base de données pour constituer le processus suivant qui est exécuté pour chacun des tests :
Puisqu'il y a toujours un premier test qui s'exécute en faisant appel à la base de données, vous n'êtes pas sûr qu'il y ait déjà des données dans les tables. PHPUnit va exécuter un TRUNCATE sur toutes les tables que vous avez indiquées pour les remettre à l'état vide.
PHPUnit va parcourir toutes les lignes de fixture indiquées et les insérer dans leurs tables respectives.
Une fois la base de données réinitialisée et remise dans son état de départ, le test en tant que tel est exécuté par PHPUnit. Cette partie du code de test ne nécessite pas du tout de s'occuper de l'extension base de données, vous pouvez procéder et tester tout ce que vous voulez dans votre code.
Votre test peut utiliser une assertion spéciale appelée
assertDataSetsEqual() à des fins de vérification,
mais c'est totalement facultatif. Cette fonctionnalité sera expliquée
dans la section « Assertions pour les bases de données ».
Habituellement quand vous utilisez PHPUnit, vos cas de tests devraient
hériter de la classe
PHPUnit_Framework_TestCase de la façon suivante :
require_once "PHPUnit/Framework/TestCase.php";
class MonTest extends PHPUnit_Framework_TestCase
{
public function testCalculate()
{
$this->assertEquals(2, 1 + 1);
}
}
Si vous voulez tester du code qui fonctionne avec l'extension base de données,
le setup sera un peu plus complexe et vous devrez hériter d'un cas de test
abstrait différent qui nécessite que vous implémentiez deux méthodes
abstraites
getConnection() et
getDataSet():
require_once "PHPUnit/Extensions/Database/TestCase.php";
class MonLivreDOrTest extends PHPUnit_Extensions_Database_TestCase
{
/**
* @return PHPUnit_Extensions_Database_DB_IDatabaseConnection
*/
public function getConnection()
{
$pdo = new PDO('sqlite::memory:');
return $this->createDefaultDBConnection($pdo, ':memory:');
}
/**
* @return PHPUnit_Extensions_Database_DataSet_IDataSet
*/
public function getDataSet()
{
return $this->createFlatXMLDataSet(dirname(__FILE__).'/_files/guestbook-seed.xml');
}
}
Pour permettre aux fonctionnalités de nettoyage et de chargement des fixtures de fonctionner, l'extension de base de données PHPUnit nécessite d'accéder à une connexion de base de données abstraite pour les différents types via la bibliothèque PDO. Il est important de noter que votre application n'a pas besoin de s'appuyer sur PDO pour utiliser l'extension de base de données de PHPUnit, la connexion est principalement utilisée pour le nettoyage et la configuration de setup.
Dans l'exemple précédent, nous avons créé une connexion Sqlite en mémoire
et nous l'avons passé à la méthode createDefaultDBConnection
qui encapsule l'instance PDO et le second paramètre (le nom de la base de données)
dans une couche d'abstraction très simple pour connexion aux bases de données du type
PHPUnit_Extensions_Database_DB_IDatabaseConnection.
La section « Utiliser la connexion de base de données » explicite l'API de cette interface et comment en faire le meilleur usage.
La méthode getDataSet() définit à quoi doit ressembler
l'état initial de la base de données avant que chaque test ne soit exécuté.
L'état de la base de données est abstrait par les concepts DataSet et DataTable,
tous les deux représentés par les interfaces
PHPUnit_Extensions_Database_DataSet_IDataSet et
PHPUnit_Extensions_Database_DataSet_IDataTable.
La prochaine section décrira en détail comment ces concepts fonctionnent
et quels sont les avantages à les utiliser lors des tests de base de données.
Pour l'implémentation, nous avons seulement besoin de savoir que la méthode
getDataSet() est appelée une fois dans
setUp() pour récupérer l'ensemble de données de la fixture
et l'insérer dans la base de données. Dans l'exemple, nous utilisons une méthode
fabrique createFlatXMLDataSet($filename) qui
représente un ensemble de données à l'aide d'une représentation XML.
PHPUnit suppose que le schéma de base de données avec toutes ses tables, ses triggers, séquences et vues est créé avant qu'un test soit exécuté. Cela signifie que vous, en tant que développeur, devez vous assurer que la base de données est correctement configurée avant de lancer la suite de tests.
Il y a plusieurs moyens pour satisfaire cette condition préalable au test de base de données.
Si vous utilisez une base de données persistante (pas Sqlite en mémoire) vous pouvez facilement configure la base de données avec des outils tels que phpMyAdmin pour MySQL et réutiliser la base de données pour chaque exécution de test.
Si vous utilisez des bibliothèques comme Doctrine 2 ou Propel vous pouvez utiliser leurs APIs pour créer le schéma de base de données dont vous avez besoin une fois avant de lancer vos tests. Vous pouvez utiliser les possibilités apportées par l'amorce et la configuration de PHPUnit pour exécuter ce code à chaque fois que vos tests sont exécutés.
En partant des exemples d'implémentation précédents, vous pouvez facilement
voir que la méthode
getConnection() est plutôt statique et peut être
réutilisée dans différents cas de test de base de données. Additionnellement
pour conserver de bonnes performances pour vos tests et maintenir la charge de la
base de données basse vous pouvez refactoriser un peu le code pour obtenir
un cas de test abstrait générique pour votre application, qui vous permette encore
d'indiquer des données de fixture différentes pour chaque cas de test :
require_once "PHPUnit/Extensions/Database/TestCase.php";
abstract class MyApp_Tests_DatabaseTestCase extends PHPUnit_Extensions_Database_TestCase
{
// instancie pdo seulement une fois pour le nettoyage du test/le chargement de la fixture
static private $pdo = null;
// instancie PHPUnit_Extensions_Database_DB_IDatabaseConnection seulement une fois par test
private $conn = null;
final public function getConnection()
{
if ($this->conn === null) {
if (self::$pdo == null) {
self::$pdo = new PDO('sqlite::memory:');
}
$this->conn = $this->createDefaultDBConnection(self::$pdo, ':memory:');
}
return $this->conn;
}
}
Mais la connexion à la base de données reste codée en dur dans la connexion PDO. PHPUnit possède une autre fonctionnalité formidable qui peut rendre ce cas de test encore plus générique. Si vous utilisez la configuration XML, vous pouvez rendre la connexion à la base de données configurable pour chaque exécution de test. Créons d'abord un fichier « phpunit.xml » dans le répertoire tests/ de l'application qui ressemble à ceci :
<?xml version="1.0" encoding="UTF-8" ?>
<phpunit>
<php>
<var name="DB_DSN" value="mysql:dbname=myguestbook;host=localhost" />
<var name="DB_USER" value="user" />
<var name="DB_PASSWD" value="passwd" />
<var name="DB_DBNAME" value="myguestbook" />
</php>
</phpunit>
Nous pouvons maintenant modifier notre cas de test pour qu'il ressemble à ça :
abstract class Generic_Tests_DatabaseTestCase extends PHPUnit_Extensions_Database_TestCase
{
// instancie pdo seulement une fois pour le nettoyage du test/le chargement de la fixture
static private $pdo = null;
// instancie PHPUnit_Extensions_Database_DB_IDatabaseConnection seulement une fois par test
private $conn = null;
final public function getConnection()
{
if ($this->conn === null) {
if (self::$pdo == null) {
self::$pdo = new PDO( $GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD'] );
}
$this->conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS['DB_DBNAME']);
}
return $this->conn;
}
}
Nous pouvons maintenant lancer la suite de tests de la base de données en utilisant différentes configurations depuis l'interface en ligne de commandes:
user@desktop> phpunit --configuration developer-a.xml MesTests/ user@desktop> phpunit --configuration developer-b.xml MesTests/
La possibilité de lancer facilement des tests de base de données sur différentes bases de données cibles est très important si vous développez sur une machine de développement. Si plusieurs développeurs exécutent les tests de base de données sur la même connexion de base de données, vous pouvez facilement faire l'expérience d'échec de tests du fait des concurrences d'accès.
Un concept centre de l'extension de base de données PHPUnit sont les DataSets et les DataTables. Vous devez comprendre ce simple concept pour maîtriser les tests de bases de données avec PHPUnit. Les DataSets et les DataTables constituent une couche d'abstraction sur les tables, les lignes et les colonnes de la base de données. Une simple API cache le contenu de la base de données sous-jacente dans une structure objet, qui peut également être implémentée par d'autres sources qui ne sont pas des bases de données.
Cette abstraction est nécessaire pour comparer le contenu constaté d'une base de données avec le contenu attendu. Les attentes peuvent être représentées dans des fichiers XML, YAML ou CSV ou des tableaux PHP par exemple. Les interfaces DataSets et DataTables permettent de comparer ces sources conceptuellement différentes en émulant un stockage en base de données relationnelle dans une approche sémantiquement similaire.
Un processus pour des assertions de base de données dans vos tests se limitera alors à trois étapes simples :
Indiquer une ou plusieurs tables dans votre base de données via leurs noms de table (ensemble de données constatées)
Indiquez l'ensemble de données attendu dans votre format préféré (YAML, XML, ..)
Affirmez que les représentations des deux ensembles de données sont égaux.
Les assertions ne constituent pas le seul cas d'utilisation des DataSets et DataTables dans l'extension de base de données PHPUnit. Comme illustré dans la section précédente, ils décrivent également le contenu initial de la base de données. Vous êtes obligés de définir un ensemble de données fixture avec le cas de test Database, qui est ensuite utilisé pour :
Supprimer toutes les lignes des tables indiquées dans le DataSet.
Ecrire toutes les lignes dans les tables de données dans la base de données.
Il existe trois types différents de datasets/datatables:
DataSets et DataTables basés sur des fichiers
DataSets et DataTables basés sur des requêtes
DataSets et DataTables de filtre et de combinaison
les datasets et les tables basés sur des fichiers sont généralement utilisés pour la fixture initiale et pour décrire l'état attendu d'une base de données.
Le dataset le plus commun est appelé XML à plat (flat XML). C'est un format
xml très simple dans lequel une balise à l'intérieur d'un noeud racine
<dataset> représente exactement une ligne de la base
de données. Les noms des balises sont ceux des tables dans lesquelles insérer les
lignes et un attribut représente la colonne. Un exemple pour une simple application
de livre d'or pourrait ressembler à ceci :
<?xml version="1.0" ?>
<dataset>
<livre_d_or id="1" contenu="Salut Poum!" utilisateur="joe" date_creation="2010-04-24 17:15:23" />
<livre_d_or id="2" contenu="J'aime !" utilisateur="nancy" date_creation="2010-04-26 12:14:20" />
</dataset>
C'est à l'évidence facile à écrire. Ici,
<livre_d_or> est le nom de la table dans laquelle les deux
lignes sont insérées, chacune avec quatre colonnes « id »,
« contenu », « utilisateur » et
« date_creation » et leurs valeurs respectives.
Cependant, cette simplicité a un coût.
Avec l'exemple précédent, difficile de voir comment nous devons indiquer une table vide. Vous pouvez insérer une balise avec aucun attribut contenant le nom de la table vide. Un fichier XML à plat pour une table livre_d_or pourrait alors ressembler à ceci:
<?xml version="1.0" ?>
<dataset>
<livre_d_or />
</dataset>
La gestion des valeurs NULL avec le dataset en XML à plat est fastidieuse. Une valeur NULL est différente d'une chaîne vide dans la plupart des bases de données (Oracle étant une exception), quelque chose qu'il est difficile de décrire dans le format XML à plat. Vous pouvez représenter une valeur NULL en omettant d'attribut indiquant la ligne. Si votre livre d'or autorise les entrées anonymes représentées par une valeur NULL dans la colonne utilisateur, un état hypothétique de la table livre_d_or pourrait ressembler à ceci:
<?xml version="1.0" ?>
<dataset>
<livre_d_or id="1" contenu="Hello Poum !" utilisateur="joe" date_creation="2010-04-24 17:15:23" />
<livre_d_or id="2" contenu="J'aime !" date_creation="2010-04-26 12:14:20" />
</dataset>
Dans ce cas, la seconde entrée est postée anonymement. Cependant, ceci conduit à un sérieux problème pour la reconnaissance de la colonne. Lors des assertions d'égalité de datasets, chaque dataset doit indiquer quelle colonne une table contient. Si un attribut est NULL pour toutes les lignes de la data-table, comment l'extension de base de données sait que la colonne doit faire partie de la table ?
Le dataset en XML à plat fait maintenant une hypothèse cruciale en décrétant que les attributs de la première ligne définie pour une table définissent les colonnes de cette table. Dans l'exemple précédent, ceci signifierait que « id », « contenu », « utilisateur » et « date_creation » sont les colonnes de la table livre_d_or. Pour la seconde ligne dans laquelle « utilisateur » n'est pas défini, un NULL sera inséré dans la base de données.
Quand la première entrée du livre d'or est supprimée du dataset, seuls « id », « contenu » et « date_creation » seront des colonnes de la table livre_d_or, puisque « utilisateur » n'est pas indiqué.
Pour utiliser efficacement le dataset au format XML à plat quand des valeurs NULL sont pertinentes, la première ligne de chaque table ne doit contenir aucune valeur NULL, seules les lignes suivantes pouvant omettre des attributs. Ceci peut s'avérer délicat, puisque l'ordre des lignes est un élément pertinent pour les assertions de base de données.
A l'inverse, si vous n'indiquez qu'un sous-élément des colonnes de la table dans le dataset au format XML à plat, toutes les valeurs omises sont positionnées à leurs valeurs par défaut. Ceci provoquera des erreurs si l'une des valeurs omises est définie par « NOT NULL DEFAULT NULL ».
En conclusion, je ne peux que vous conseiller de n'utiliser les datasets au format XML à plat que si vous n'avez pas besoin des valeurs NULL.
Vous pouvez créer une instance de dataset au format XML à plat
dans votre cas de test de base de données en appelant la méthode
createFlatXmlDataSet($filename):
class MonCasDeTest extends PHPUnit_Extensions_Database_TestCase
{
public function getDataSet()
{
return $this->createFlatXmlDataSet('maFixtureAuFormatXMLaPlat.xml');
}
}
Il existe un autre dataset XML davantage structuré, qui est un peu plus
verbeux à écrire mais qui évite les problèmes de NULL du dataset au
format XML à plat. Dans le noeud racine <dataset> vous
pouvez indiquer les balises <table>,
<column>, <row>,
<value> et
<null />. Un dataset équivalent à celui
défini précédemment pour le livre d'or en format XML à plat ressemble à :
<?xml version="1.0" ?>
<dataset>
<table name="livre_d_or">
<column>id</column>
<column>contenu</column>
<column>utilisateur</column>
<column>date_creation</column>
<row>
<value>1</value>
<value>Hello Poum !</value>
<value>joe</value>
<value>2010-04-24 17:15:23</value>
</row>
<row>
<value>2</value>
<value>J'aime !</value>
<null />
<value>2010-04-26 12:14:20</value>
</row>
</table>
</dataset>
Tout <table> défini possède un nom et nécessite
la définition de toutes les colonnes avec leurs noms. Il peut contenir zéro ou tout
nombre positif d'éléments <row> imbriqués.
Ne définir aucun élément <row> signifie que la table est vide.
Les balises <value> et
<null /> doivent être indiquées dans l'ordre des éléments
<column>précédemment donnés.
La balise <null /> signifie évidemment que la valeur est NULL.
Vous pouvez créer une instance de dataset xml dans votre
cas de test de base de données en appelant la méthode
createXmlDataSet($filename) :
class MonCasdeTest extends PHPUnit_Extensions_Database_TestCase
{
public function getDataSet()
{
return $this->createXMLDataSet('maFixtureyXml.xml');
}
}
Ce nouveau format XML est spécifique au
serveur de bases de données MySQL.
Sa gestion a été ajoutée dans PHPUnit 3.5. Les fichiers écrits ce format peuvent
être générés avec l'utilitaire
mysqldump.
Contrairement aux datasets CSV, que mysqldump
gère également, un unique fichier de ce format XML peut contenir des données
pour de multiples tables. Vous pouvez créer un fichier dans ce format en
invoquant mysqldump de cette façon :
mysqldump --xml -t -u [nom_utilisateur] --password=[mot_de_passe] [base_de_donnees] > /chemin/vers/fichier.xml
Ce fichier peut être utilisé dans votre case de test de base de données en appelant
la méthode createMySQLXMLDataSet($nomdefichier):
class MonCasDeTest extends PHPUnit_Extensions_Database_TestCase
{
public function getDataSet()
{
return $this->createMySQLXMLDataSet('/chemin/vers/fichier.xml');
}
}
Nouveau depuis PHUnit 3.4, la possibilité d'indiquer un dataset via le format populaire YAML. Pour que cela fonctionne, vous devez installer PHPUnit 3.4 avec PEAR ainsi que sa dépendance optionnelle Symfony YAML. Vous pouvez ensuite écrire un dataset YAML pour l'exemple du livre d'or:
livre_d_or:
-
id: 1
contenu: "Hello Poum !"
utilisateur: "joe"
date_creation: 2010-04-24 17:15:23
-
id: 2
contenu: "J'aime !"
utilisateur:
date_creation: 2010-04-26 12:14:20
C'est simple, pratique et ça règle le problème de NULL que pose le dataset
équivalent au format XML à plat. Un NULL en YAML s'exprime simplement
en donnant le nom de la colonne dans indiquer de valeur. Une chaîne vide
est indiquée par
colonne1: "".
Le dataset YAML ne possède pas actuellement de méthode de fabrique pour le cas de tests de base de données, si bien que vous devez l'instancier manuellement :
require_once "PHPUnit/Extensions/Database/DataSet/YamlDataSet.php";
class LivredOrYamlTest extends PHPUnit_Extensions_Database_TestCase
{
protected function getDataSet()
{
return new PHPUnit_Extensions_Database_DataSet_YamlDataSet(
dirname(__FILE__)."/_files/livre_d_or.yml"
);
}
}
Un autre dataset au format fichier est basé sur les fichiers CSV. Chaque table du dataset est représenté par un fichier CSV. Pour notre exemple de livre d'or, nous pourrions définir un fichier livre_d_or-table.csv:
id;contenu;utilisateur;date_creation 1;"Hello Poum !";"joe";"2010-04-24 17:15:23" 2;"J'aime !""nancy";"2010-04-26 12:14:20"
Bien que ce soit très pratique à éditer avec Excel ou LibreOffice, vous ne pouvez pas indiquer de valeurs NULL avec le dataset CSV. Une colonne vide conduira à ce que la valeur vide par défaut de la base de données soit insérée dans la colonne.
Vous pouvez créer un dataset CSV en appelant :
require_once 'PHPUnit/Extensions/Database/DataSet/CsvDataSet.php';
class CsvLivredOrTest extends PHPUnit_Extensions_Database_TestCase
{
protected function getDataSet()
{
$dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet();
$dataSet->addTable('guestbook', dirname(__FILE__)."/_files/livre_d_or.csv");
return $dataSet;
}
}
Il n'existe pas (encore) de DataSet basé sur les tableau dans l'extension base de données de PHPUnit, mais vous pouvez implémenter facilement la vôtre. Notre exemple du Livre d'or devrait ressembler à :
class LivredOrTableauTest extends PHPUnit_Extensions_Database_TestCase
{
protected function getDataSet()
{
return new MyApp_DbUnit_ArrayDataSet(array(
'livre_d_or' => array(
array('id' => 1, 'contenu' => 'Hello Poum !', 'utilisateur' => 'joe', 'date_creation' => '2010-04-24 17:15:23'),
array('id' => 2, 'contenu' => 'J\'aime !', 'utilisateur' => null, 'date_creation' => '2010-04-26 12:14:20'),
),
));
}
}
Un DataSet PHP possède des avantages évidents sur les autres datasets utilisant des fichiers :
Les tableaux PHP peuvent évidemment gérer les valeurs NULL.
Vous n'avez pas besoin de fichiers additionnels pour les assertions et vous pouvez les renseigner directement dans les cas de test.
Pour que ce dataset ressemble aux DataSets au format XML à plat, CSV et YAML, les clefs de la première ligne spécifiée définissent les noms de colonne de la table, dans le cas précédent, ce serait « contenu », « utilisateur » et « date_creation ».
L'implémentation de ce DataSet tableau est simple et évidente:
require_once 'PHPUnit/Util/Filter.php';
require_once 'PHPUnit/Extensions/Database/DataSet/AbstractDataSet.php';
require_once 'PHPUnit/Extensions/Database/DataSet/DefaultTableIterator.php';
require_once 'PHPUnit/Extensions/Database/DataSet/DefaultTable.php';
require_once 'PHPUnit/Extensions/Database/DataSet/DefaultTableMetaData.php';
PHPUnit_Util_Filter::addFileToFilter(__FILE__, 'PHPUNIT');
class MyApp_DbUnit_ArrayDataSet extends PHPUnit_Extensions_Database_DataSet_AbstractDataSet
{
/**
* @var array
*/
protected $tables = array();
/**
* @param array $data
*/
public function __construct(array $data)
{
foreach ($data AS $tableName => $rows) {
$columns = array();
if (isset($rows[0])) {
$columns = array_keys($rows[0]);
}
$metaData = new PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData($tableName, $columns);
$table = new PHPUnit_Extensions_Database_DataSet_DefaultTable($metaData);
foreach ($rows AS $row) {
$table->addRow($row);
}
$this->tables[$tableName] = $table;
}
}
protected function createIterator($reverse = FALSE)
{
return new PHPUnit_Extensions_Database_DataSet_DefaultTableIterator($this->tables, $reverse);
}
public function getTable($tableName)
{
if (!isset($this->tables[$tableName])) {
throw new InvalidArgumentException("$tableName ne correspond pas à une table dans la base de données actuelle.");
}
return $this->tables[$tableName];
}
}
Pour les assertions de base de données, vous n'avez pas seulement besoin de datasets basés sur des fichiers mais aussi de Datasets basé sur des requêtes/du SQL qui contiennent le contenu constaté de la base de données. C'est là que le DataSet Query s'illustre :
$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('livre_d_or');
Ajouter une table juste par son nom est un moyen implicite de définir la table de données avec la requête suivante :
$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('livre_d_or', 'SELECT * FROM livre_d_or');
Vous pouvez utiliser ceci en indiquant des requêtes arbitraires pour
vos tables, par exemple en restreignant les lignes, les colonnes ou en
ajoutant des clauses ORDER BY:
$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('livre_d_or', 'SELECT id, contenu FROM livre_d_or ORDER BY date_creation DESC');
La section relative aux assertions de base de données montrera plus en détails comment utiliser le Query DataSet.
En accédant à la connexion de test, vous pouvez créer automatiquement un DataSet constitué de toutes les tables et de leur contenu de la base de données indiquée comme second paramètre de la méthode fabrique de connexion.
Vous pouvez, soit créer un dataset pour la base de données complète
comme montré dans la méthode testLivredOr(), soit le restreindre
à un ensemble de noms de tables avec une liste blanche comme montré dans
la méthode testLivredOrFiltre().
class MonLivredOrSqlTest extends PHPUnit_Extensions_Database_TestCase
{
/**
* @return PHPUnit_Extensions_Database_DB_IDatabaseConnection
*/
public function getConnection()
{
$base_de_donnees = 'ma_base_de_donnee';
$pdo = new PDO('mysql:...', $utilisateur, $mot_de_passe);
return $this->createDefaultDBConnection($pdo, $base_de_donnees);
}
public function testLivredOr()
{
$dataSet = $this->getConnection()->createDataSet();
// ...
}
public function testLivredOrFiltre()
{
$nomTables = array('livre_d_or');
$dataSet = $this->getConnection()->createDataSet($nomTables);
// ...
}
}
J'ai évoqué les problèmes de NULL avec les DataSet au format XML à plat et CSV, mais il y existe un contournement légèrement compliqué pour que ces deux types de datasets fonctionnent avec NULLs.
Le DataSet de remplacement est un décorateur pour un dataset existant et vous permet de remplacer des valeurs dans toute colonne du dataset par une autre valeur de remplacement. Pour que notre exemple de livre d'or fonctionne avec des valeurs NULL nous indiquons le fichier comme ceci:
<?xml version="1.0" ?>
<dataset>
<livre_d_or id="1" contenu="Hello Poum !" utilisateur="joe" date_creation="2010-04-24 17:15:23" />
<livre_d_or id="2" contenu="J'aime !" utilisateur="##NULL##" date_creation="2010-04-26 12:14:20" />
</dataset>
Nous enrobons le DataSet au format XML à plat dans le DataSet de remplacement :
require_once 'PHPUnit/Extensions/Database/DataSet/ReplacementDataSet.php';
class ReplacementTest extends PHPUnit_Extensions_Database_TestCase
{
public function getDataSet()
{
$ds = $this->createFlatXmlDataSet('maFixtureEnXMLaPlat.xml');
$rds = new PHPUnit_Extensions_Database_DataSet_ReplacementDataSet($ds);
$rds->addFullReplacement('##NULL##', null);
return $rds;
}
}
Si vous avez un fichier de fixture conséquent vous pouvez utiliser le filtre de DataSet pour des listes blanches ou noires des tables et des colonnes qui peuvent être contenues dans un sous-dataset. C'est particulièrement commode en combinaison avec le DataSet de base de données pour filtrer les colonnes des datasets.
class DataSetFilterTest extends PHPUnit_Extensions_Database_TestCase
{
public function testLivredOrAvecFiltredInclusion()
{
$nomTables = array('livre_d_or');
$dataSet = $this->getConnection()->createDataSet();
$filterDataSet = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($dataSet);
$filterDataSet->addIncludeTables(array('livre_d_or'));
$filterDataSet->setIncludeColumnsForTable('livre_d_or', array('id', 'contenu'));
// ..
}
public function testLivredOrAvecFiltredExclusion()
{
$nomTables = array('livre_d_or');
$dataSet = $this->getConnection()->createDataSet();
$filterDataSet = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($dataSet);
$filterDataSet->addExcludeTables(array('foo', 'bar', 'baz')); // ne garder que la table livre_d_or !
$filterDataSet->setExcludeColumnsForTable('livre_d_or', array('utilisateur', 'date_creationd'));
// ..
}
}
NOTE Vous ne pouvez pas utiliser en même temps le filtrage de colonne d'inclusion et d'exclusion sur la même table, seulement sur des tables différentes. De plus, il est seulement possible d'appliquer soit une liste blanche, soit une liste noire aux tables, mais pas les deux à la fois.
Le DataSet composite est très utile pour agréger plusieurs datasets déjà existants dans un unique dataset. Quand plusieurs datasets contiennent la même table, les lignes sont ajoutées dans l'ordre indiqué. Par exemple, si nous avons deux datasets fixture1.xml :
<?xml version="1.0" ?>
<dataset>
<livre_d_or id="1" contenu="Hello Poum !" utilisateur="joe" date_creation="2010-04-24 17:15:23" />
</dataset>
et fixture2.xml:
<?xml version="1.0" ?>
<dataset>
<livre_d_or id="2" contenu="J'aime !" utilisateur="##NULL##" date_creation="2010-04-26 12:14:20" />
</dataset>
En utiliser le DataSet composite, nous pouvons agréger les deux fichiers de fixture:
class CompositeTest extends PHPUnit_Extensions_Database_TestCase
{
public function getDataSet()
{
$ds1 = $this->createFlatXmlDataSet('fixture1.xml');
$ds2 = $this->createFlatXmlDataSet('fixture2.xml');
$compositeDs = new PHPUnit_Extensions_Database_DataSet_CompositeDataSet();
$compositeDs->addDataSet($ds1);
$compositeDs->addDataSet($ds2);
return $compositeDs;
}
}
Lors du SetUp de la fixture l'extension de base de données de PHPUnit insère les lignes dans la base de données dans l'ordre où elles sont indiquées dans votre fixture. Si votre schéma de base de données utilise des clefs étrangères, ceci signifie que vous devez indiquer les tables dans un ordre qui ne provoquera pas une violation de contrainte pour ces clefs étrangères.
Pour comprendre le fonctionnement interne des DataSets et des DataTables jetons un oeil sur l'interface d'un DataSet. Vous pouvez sauter cette partie si vous ne projetez pas d'implémenter votre propre DataSet ou DataTable.
interface PHPUnit_Extensions_Database_DataSet_IDataSet extends IteratorAggregate
{
public function getTableNames();
public function getTableMetaData($nomTable);
public function getTable($nomTable);
public function assertEquals(PHPUnit_Extensions_Database_DataSet_IDataSet $autre);
public function getReverseIterator();
}
L'interface publique est utilisée en interne par l'assertion
assertDataSetsEqual() du cas de test de base de données
pour contrôler la qualité du dataset. De l'interface
IteratorAggregate le IDataSet
hérite la méthode getIterator() pour parcourir toutes
les tables du dataset. La méthode additionnelle d'itérateur inverse est
nécessaire pour réussir à tronquer les tables dans l'ordre inverse à celui indiqué.
Pour comprendre le besoin d'un itérateur inverse, pensez deux tables (TableA et TableB) avec TableB qui contient une clef étrangère sur une colonne de TableA. Si pour la configuration de la fixture une ligne est insérée dans TableA puis un enregistrement dépendant dans TableB, alors il est évident que pour détruire le contenu de toutes les tables, l'ordre inverse va vous poser des problèmes avec les contraintes de clefs étrangères.
En fonction de l'implémentation, différentes approches sont prises
pour ajouter des instances de table dans un dataset. Par exemple,
les tables sont ajoutées de façon interne lors de la construction
depuis le fichier source dans tous les datasets basés sur les fichiers comme
YamlDataSet, XmlDataSet ou FlatXmlDataSet.
Une table est également représentée par l'interface suivante :
interface PHPUnit_Extensions_Database_DataSet_ITable
{
public function getTableMetaData();
public function getRowCount();
public function getValue($row, $column);
public function getRow($row);
public function assertEquals(PHPUnit_Extensions_Database_DataSet_ITable $other);
}
Mise à part la méthode getTableMetaData(), ça parle
plutôt de soi-même. Les méthodes utilisées sont toutes nécessaires pour
les différentes assertions de l'extension Base de données expliquées
dans le chapitre suivant. La méthode
getTableMetaData() doit retourner une implémentation de
l'interface PHPUnit_Extensions_Database_DataSet_ITableMetaData
qui décrit la structure de la table. Elle contient des informations sur:
Le nom de la table
Un tableau des noms de colonne de la table, classé par leur ordre d'apparition dans l'ensemble résultat.
Un tableau des colonnes clefs primaires.
Cette interface possède également une assertion qui contrôle si deux instances des méta données des tables sont égales et qui sera utilisée par l'assertion d'égalité d'ensemble de données.
Il y a trois méthodes intéressantes dans l'interface de connexion
qui doit être retournée par la méthode
getConnection() du cas de test de base de données :
interface PHPUnit_Extensions_Database_DB_IDatabaseConnection
{
public function createDataSet(Array $tableNames = NULL);
public function createQueryTable($resultName, $sql);
public function getRowCount($tableName, $whereClause = NULL);
// ...
}
La méthode createDataSet() crée un DataSet de base de données
(DB) comme décrit dans la section relative aux implémentations de DataSet.
class ConnectionTest extends PHPUnit_Extensions_Database_TestCase
{
public function testCreateDataSet()
{
$tableNames = array('livre_d_or');
$dataSet = $this->getConnection()->createDataSet();
}
}
La méthode createQueryTable() peut être utilisée pour
créer des instances d'une QueryTable, en lui passant un nom de résultat et
une requête SQL. C'est une méthode pratique quand elle est associée à des
assertions résultats/table comme cela sera illustré dans la prochaine section
relative à l'API des assertions de base de données.
class ConnectionTest extends PHPUnit_Extensions_Database_TestCase
{
public function testCreateQueryTable()
{
$nomTables = array('livre_d_or');
$queryTable = $this->getConnection()->createQueryTable('livre_d_or', 'SELECT * FROM livre_d_or');
}
}
La méthode getRowCount() est un moyen pratique d'accéder
au nombre de lignes d'une table, éventuellement filtrées par une
clause where supplémentaire. Ceci peut être utilisé pour une simple
assertion d'égalité :
class ConnectionTest extends PHPUnit_Extensions_Database_TestCase
{
public function testGetRowCount()
{
$this->assertEquals(2, $this->getConnection()->getRowCount('livre_d_or'));
}
}
En tant qu'outil de test, l'extension base de données fournit certainement des assertions que vous pouvez utiliser pour vérifier l'état actuel de la base de données, des tables et du nombre de lignes des tables. Cette section décrit ces fonctionnalités en détail :
Il est souvent très utile de vérifier si une table contient un nombre déterminé de lignes. Vous pouvez facilement réaliser cela sans code de liaison supplémentaire en utilisant l'API de connexion. Disons que nous voulons contrôler qu'après une insertion d'une ligne dans notre livre d'or, nous n'avons plus seulement nos deux entrées initiales qui nous ont accompagnées dans tous les exemples précédents, mais aussi une troisième :
class LivredorTest extends PHPUnit_Extensions_Database_TestCase
{
public function testAddEntry()
{
$this->assertEquals(2, $this->getConnection()->getRowCount('livre_d_or'), "Pré-Condition");
$livre_d_or = new Livredor();
$livre_d_or->addEntry("suzy", "Hello world!");
$this->assertEquals(3, $this->getConnection()->getRowCount('livre_d_or'), "Insertion en échec");
}
}
L'assertion précédent est utile, mais nous voudrons certainement tester le contenu présent de la table pour vérifier que toutes les valeurs ont été écrites dans les bonnes colonnes. Ceci peut être réalisé avec une assertion de table.
Pour cela, nous devons définir une instance de Query Table qui tire son contenu d'un nom de table et d'une requête SQL et le compare à un DataSet basé sur un fichier/tableau.
class LivredorTest extends PHPUnit_Extensions_Database_TestCase
{
public function testAddEntry()
{
$livre_d_or = new Livredor();
$livre_d_or->addEntry("suzy", "Hello world!");
$requeteDeTable = $this->getConnection()->createQueryTable(
'livre_d_or', 'SELECT * FROM livre_d_or'
);
$tableAttendue = $this->createFlatXmlDataSet("livreAttendu.xml")
->getTable("livre_d_or");
$this->assertTablesEqual($tableAttendue, $requeteDeTable);
}
}
Maintenant, nous devons écrire le fichier XML à plat livreAttendu.xml pour cette assertion :
<?xml version="1.0" ?>
<dataset>
<livre_d_or id="1" contenu="Hello Poum !" utilisateur="joe" date_creation="2010-04-24 17:15:23" />
<livre_d_or id="2" contenu="J'aime !" utilisateur="nancy" date_creation="2010-04-26 12:14:20" />
<livre_d_or id="3" contenu="Hello world!" utilisateur="suzy" date_creation="2010-05-01 21:47:08" />
</dataset>
Cette assertion ne réussira que si elle est lancée très exactement le 2010–05–01 21:47:08. Les dates posent un problème spécial pour le test de base de données et nous pouvons contourner l'échec en omettant la colonne « date_creation » de l'assertion.
Le fichier au format XML à plat adapté livreAttendu.xml devra probablement ressembler à ce qui suit pour que l'assertion réussisse.
<?xml version="1.0" ?>
<dataset>
<livre_d_or id="1" contenu="Hello Poum !" utilisateur="joe" />
<livre_d_or id="2" contenu="J'aime !" utilisateur="nancy" />
<livre_d_or id="3" contenu="Hello world!" utilisateur="suzy" />
</dataset>
Nous devons corriger l'appel à Query Table:
$queryTable = $this->getConnection()->createQueryTable(
'livre_d_or', 'SELECT id, contenu, utilisateur FROM livre_d_or'
);
Vous pouvez également faire une assertion sur le résultat de requêtes complexes avec l'approche Query Table, simplement en indiquant le nom d'un résultat avec une requête et en le comparant avec un ensemble de données:
class ComplexQueryTest extends PHPUnit_Extensions_Database_TestCase
{
public function testComplexQuery()
{
$requeteTable = $this->getConnection()->createQueryTable(
'maRequeteComplexe', 'SELECT requeteComplexe..'
);
$tableAttendue = $this->createFlatXmlDataSet("assertionDeRequeteComplexe.xml")
->getTable("myComplexQuery");
$this->assertTablesEqual($tableAttendue, $requeteTable);
}
}
Evidemment, vous pouvez faire une assertion sur l'état de plusieurs tables à la fois et comparer un ensemble de données obtenu par une requête avec un ensemble de données basé sur un fichier. Il y a deux façons différentes de faire des assertions de DataSet.
Vous pouvez utiliser le Database (DB) Dataset à partir de la connexion et le comparer au DataSet basé sur un fichier.
class DataSetAssertionsTest extends PHPUnit_Extensions_Database_TestCase
{
public function testCreateDataSetAssertion()
{
$dataSet = $this->getConnection()->createDataSet(array('livre_d_or'));
$dataSetAttendu = $this->createFlatXmlDataSet('livre_d_or.xml');
$this->assertDataSetsEqual($dataSetAttendu, $dataSet);
}
}
Vous pouvez construire vous-même le DataSet:
class DataSetAssertionsTest extends PHPUnit_Extensions_Database_TestCase
{
public function testManualDataSetAssertion()
{
$dataSet = new PHPUnit_Extensions_Database_DataSet_QueryDataSet();
$dataSet->addTable('livre_d_or', 'SELECT id, contenu, utilisateur FROM livre_d_or'); // tables supplémentaires
$dataSetAttendu = $this->createFlatXmlDataSet('livre_d_or.xml');
$this->assertDataSetsEqual($dataSetAttendu, $dataSet);
}
}
Non, PHPUnit exige que tous les objets de base de données soit disponible quand la suite démarre. La base de données, les tables, les séquences, les triggers et les vues doivent être créés avant que vous exécutiez la suite de tests.
Doctrine 2 ou eZ Components possèdent des outils puissants qui vous permettent de créer le schéma de base de données à partir de structures de données définies préalablement, cependant, ceux-ci doivent être reliés à l'extension PHPUnit pour permettre la recréation automatique de la base de données avant que la suite de tests complète ne soit exécutée.
Puisque chaque test nettoie complètement la base de données, vous n'avez même pas obligation de re-créer la base de donnée pour chaque exécution des tests. Une base de données disponible de façon permanente fonctionne parfaitement.
Non, PDO n'est nécessaire que pour le nettoyage et la configuration de la fixture et pour les assertions. Vous pouvez utiliser n'importe laquelle des abstractions de base de données que vous voulez dans votre propre code.
Si vous ne mettez pas en cache l'instance PDO qui est créée dans
la méthode getConnection() du cas de test
le nombre de connexions à la base de données est augmenté d'une unité ou plus
pour chaque test de base de données. Avec la configuration par défaut, MySql
n'autorise qu'un maximum de 100 connexions concurrentes. Les autres moteurs
de bases de données possèdent également des limites du nombre maximum
de connexions.
La sous-section « Utilisez votre propre cas de test de base de données abstrait » illustre comment vous pouvez empêcher cette erreur de survenir en utilisant une unique instance de PDO en cache dans tous vos tests.
Quand vous travaillez sur une nouvelle classe de cas de test, vous pourriez vouloir commencer en écrivant des méthodes de test vides comme
public function testQuelquechose()
{
} pour garder la trace des tests que vous avez à écrire. Le problème avec
les méthodes de test vides est qu'elles sont interprétées comme étant réussies par le
framework PHPUnit. Cette mauvaise interprétation fait que le rapport de tests devient
inutile -- vous ne pouvez pas voir si un test est effectivement réussi ou s'il n'a tout
simplement pas été implémenté. Appeler
$this->fail() dans une méthode de test non implémentée
n'aide pas davantage, puisqu'alors le test sera interprété comme étant un échec.
Ce serait tout aussi faux que d'interpréter un test non implémenté comme étant réussi.
Si nous pensons à un test réussi comme à un feu vert et à un échec de test
comme à un feu rouge, nous avons besoin d'un feu orange additionnel pour signaler
un test comme étant incomplet ou pas encore implémenté.
PHPUnit_Framework_IncompleteTest est une interface de marquage
pour signaler une exception qui est levée par une méthode de test comme résultat
d'un test incomplet ou actuellement pas implémenté.
PHPUnit_Framework_IncompleteTestError est l'implémentation
standard de cette interface.
Exemple 9.1, « Signaler un test comme incomplet »
montre une classe de cas de tests, ExempleDeTest, qui contient une unique méthode de test,
method, testSomething(). En appelant la méthode pratique
markTestIncomplete() (qui lève automatiquement
une exception PHPUnit_Framework_IncompleteTestError)
dans la méthode de test, nous marquons le test comme étant incomplet.
Exemple 9.1. Signaler un test comme incomplet
<?php
class ExempleDeTest extends PHPUnit_Framework_TestCase
{
public function testeQuelquechose()
{
// Facultatif: testez tout ce que vous voulez ici.
$this->assertTrue(TRUE, 'Ceci devrait toujours fonctionner.');
// Cesser ici et marquer ce test comme incomplet.
$this->markTestIncomplete(
'Ce test n'a pas encore été implémenté.'
);
}
}
?>
Un test incomplet est signalé par un I sur la sortie écran
du lanceur de test en ligne de commandes PHPUnit, comme montré dans l'exemple
suivant.
phpunit --verbose ExempleDeTest
PHPUnit 3.6.0 by Sebastian Bergmann.
I
Time: 0 seconds, Memory: 3.75Mb
There was 1 incomplete test:
1) ExempleDeTest::testQuelquechose
This test has not been implemented yet.
/home/sb/ExempleDeTest.php:12
OK, but incomplete or skipped tests!
Tests: 1, Assertions: 1, Incomplete: 1.Tableau 9.1, « API pour les tests incomplets » montre l'API pour marquer des tests comme incomplets.
Tableau 9.1. API pour les tests incomplets
| Méthode | Signification |
|---|---|
void markTestIncomplete() | Marque le test courant comme incomplet. |
void markTestIncomplete(string $message) | Marque le test courant comme incomplet en utilisant $message comme message d'explication. |
Tous les tests ne peuvent pas être exécutés dans tous les environnements. Considérez, par exemple, une couche d'abstraction de base de données qui possède différents pilotes pour les différents systèmes de base de données qu'elle gère. Les tests pour le pilote MySQL ne peuvent bien sûr être exécutés que si un serveur MySQL est disponible.
Exemple 9.2, « Sauter un test »
montre une classe de cas de tests, DatabaseTest, qui contient une méthode de tests
testConnection(). Dans la méthode canevas setUp()
de la classe du cas de test, nous pouvons contrôler si l'extension
MySQLi est disponible et utiliser la méthode markTestSkipped()
pour sauter le test si ce n'est pas le cas.
Exemple 9.2. Sauter un test
<?php
class DatabaseTest extends PHPUnit_Framework_TestCase
{
protected function setUp()
{
if (!extension_loaded('mysqli')) {
$this->markTestSkipped(
'L\'extension MySQLi n\'est pas disponible.'
);
}
}
public function testConnection()
{
// ...
}
}
?>
Un test qui a été sauté est signalé par un S dans la sortie
écran du lanceur de tests en ligne de commande PHPUnit, comme montré dans
l'exemple suivant.
phpunit --verbose DatabaseTest
PHPUnit 3.6.0 by Sebastian Bergmann.
S
Time: 0 seconds, Memory: 3.75Mb
There was 1 skipped test:
1) DatabaseTest::testConnection
The MySQLi extension is not available.
/home/sb/DatabaseTest.php:9
OK, but incomplete or skipped tests!
Tests: 1, Assertions: 0, Skipped: 1.Tableau 9.2, « API pour sauter des tests » montre l'API pour sauter des tests.
Tableau 9.2. API pour sauter des tests
| Méthode | Signification |
|---|---|
void markTestSkipped() | Marque le test courant comme sauté. |
void markTestSkipped(string $message) | Marque le test courant comme étant sauté en utilisant $message comme message d'explication. |
Gerard Meszaros introduit le concept de doublure de test dans [Meszaros2007] comme ceci:
La méthode getMock($nomClasse) fournit par PHPUnit peut être
utilisée dans un test pour générer automatiquement un objet qui peut agir comme une
doublure de test pour une classe originelle indiquée. Cette doublure de test peut être
utilisée dans tous les contextes où la classe originelle est attendue.
Par défaut, toutes les méthodes de la classe originelle sont remplacées
par une implémentation fictive qui se contente de retourner
NULL (sans appeler la méthode originelle).
En utilisant la méthode will($this->returnValue())
par exemple, vous pouvez configurer ces implémentations fictives pour
retourner une valeur donnée quand elles sont appelées.
Merci de noter que les méthodes final, private
et static ne peuvent pas être remplacées par un bouchon (stub) ou un simulacre (mock). Elles seront
ignorées par la fonction de doublure de test de PHPUnit et conserveront leur comportement
initial.
La pratique consistant à remplacer un objet par une doublure de test qui retourne (de façon facultative) des valeurs de retour configurées est appelée bouchonnage. Vous pouvez utiliser un bouchon pour "remplacer un composant réel dont dépend le système testé de telle façon que le test possède un point de contrôle sur les entrées indirectes dans le SCT. Ceci permet au test de forcer le SCT à utiliser des chemins qu'il n'aurait pas emprunté autrement".
Exemple 10.2, « Bouchonner un appel de méthode pour retourner une valeur fixée » montre comment
la méthode de bouchonnage appelle et configure des valeurs de retour. Nous utilisons
d'abord la méthode getMock() qui est fournie par la classe
PHPUnit_Framework_TestCase pour configurer un objet bouchon
qui ressemble à un objet de UneClasse
(Exemple 10.1, « La classe que nous voulons bouchonner »). Ensuite nous
utilisons l'interface souple
que PHPUnit fournit pour indiquer le comportement de ce bouchon. En essence,
cela signifie que vous n'avez pas besoin de créer plusieurs objets temporaires
et les relier ensemble ensuite. Au lieu de cela, vous chaînez les appels de méthode
comme montré dans l'exemple. Ceci amène à un code plus lisible et "souple".
Exemple 10.1. La classe que nous voulons bouchonner
<?php
class UneClasse
{
public function faireQuelquechose()
{
// Faire quelque chose.
}
}
?>
Exemple 10.2. Bouchonner un appel de méthode pour retourner une valeur fixée
<?php
require_once 'UneClasse.php';
class BouchonTest extends PHPUnit_Framework_TestCase
{
public function testBouchon()
{
// Créer un bouchon pour la classe UneClasse.
$bouchon = $this->getMock('UneClasse');
// Configurer le bouchon.
$bouchon->expects($this->any())
->method('faireQuelquechose')
->will($this->returnValue('foo'));
// Appeler $bouchon->faireQuelquechose() va maintenant retourner
// 'foo'.
$this->assertEquals('foo', $bouchon->faireQuelquechose());
}
}
?>
"Derrière la scène", PHPUnit génère automatiquement une nouvelle classe qui
implémente le comportement souhaité quand la méthode getMock()
est utilisée. La classe doublure de test peut être configurée via des
paramètres optionnels de la méthode getMock().
Par défaut, toutes les méthodes d'une classe données sont remplacées par une doublure de test qui retourne simplement NULL à moins qu'une valeur de retour ne soit configurée en utilisant will($this->returnValue()), par exemple.
Quand le deuxième paramètre (facultatif) est fourni, seules les méthodes dont les noms sont dans le tableau sont remplacées par une doublure de test configurable. Le comportement des autres méthodes n'est pas modifié.
Le troisième paramètre (facultatif) peut contenir un tableau de paramètre qui est passé dans le constructeur de la classe originelle (qui n'est pas remplacé par une implémentation fictive par défaut).
Le quatrième paramètre (facultatif) peut être utilisé pour indiquer un nom de classe pour la classe de doublure de test générée.
Le cinquième paramètre (facultatif) peut être utilisée pour désactiver l'appel du constructeur de la classe originelle.
Le sixième paramètre (facultatif) peut être utilisé pour désactiver l'appel au constructeur du clone de la classe originelle.
Le septième paramètre (facultatif) peut être utilisé pour désactiver __autoload() lors de la génération de la classe de doublure de test.
Alternativement, l'API Mock Builder peut être utilisé pour configurer la classe de doublure de test générée. Exemple 10.3, « Utiliser l'API Mock Builder pour configurer la classe de doublure de test générée. » montre un exemple. Ici, il y a une liste de méthodes qui peuvent être utilisées avec l'interface de Mock Builder:
setMethods(array $methodes) peut être appelée sur l'objet Mock Builder pour indiquer les méthodes qui doivent être remplacées par une doublure de test configurable. Le comportement des autres méthodes n'est pas modifié.
setConstructorArgs(array $parametres) peut être appelé pour fournir un paramètre tableau qui est passé au constructeur de la classe originelle (qui n'est pas remplacé par une implémentation fictive par défaut).
setMockClassName($nom) peut être utilisée pour indiquer un nom de classe pour la classe de doublure de test générée.
disableOriginalConstructor() peut être utilisé pour désactiver l'appel au constructeur de la classe originelle.
disableOriginalClone() peut être utilisé pour désactiver l'appel au constructeur clone de la classe originelle.
disableAutoload() peut être utilisée pour désactiver __autoload() lors de la génération de la classe de doublure de test.
Exemple 10.3. Utiliser l'API Mock Builder pour configurer la classe de doublure de test générée.
<?php
require_once 'UneClasse.php';
class BouchonTest extends PHPUnit_Framework_TestCase
{
public function testBouchon()
{
// Créer un bouchon pour la classe UneClasse.
$bouchon = $this->getMockBuilder('UneClasse')
->disableOriginalConstructor()
->getMock();
// Configure le bouchon.
$bouchon->expects($this->any())
->method('faireQuelquechose')
->will($this->returnValue('foo'));
// Appeler $bouchon->faireQuelquechose() retournera maintenant
// 'foo'.
$this->assertEquals('foo', $bouchon->faireQuelquechose());
}
}
?>
Parfois vous voulez renvoyer l'un des paramètres d'un appel de méthode
(non modifié) comme résultat d'un appel méthode bouchon.
Exemple 10.4, « Bouchonner un appel de méthode pour renvoyer un des paramètres » montre comment vous
pouvez obtenir ceci en utilisant returnArgument() à la place de
returnValue().
Exemple 10.4. Bouchonner un appel de méthode pour renvoyer un des paramètres
<?php
require_once 'UneClasse.php';
class BouchonTest extends PHPUnit_Framework_TestCase
{
public function testReturnArgumentBouchon()
{
// Créer un bouchon pour la classe UneClasse.
$bouchon = $this->getMock('UneClasse');
// Configurer le bouchon.
$bouchon->expects($this->any())
->method('faireQuelquechose')
->will($this->returnArgument(0));
// $bouchon->faireQuelquechose('foo') retourne 'foo'
$this->assertEquals('foo', $bouchon->faireQuelquechose('foo'));
// $bouchon->faireQuelquechose('bar') retourne 'bar'
$this->assertEquals('bar', $bouchon->faireQuelquechose('bar'));
}
}
?>
Quand on teste interface souple, il est parfois utile que la méthode bouchon
retourne une référence à l'objet bouchon.
Exemple 10.5, « Bouchonner un appel de méthode pour renvoyer une référence de l'objet bouchon. » illustre comment vous
pouvez utiliser returnSelf() pour accomplir cela.
Exemple 10.5. Bouchonner un appel de méthode pour renvoyer une référence de l'objet bouchon.
<?php
require_once 'UneClasse.php';
class BouchonTest extends PHPUnit_Framework_TestCase
{
public function testReturnSelf()
{
// Créer un bouchon pour la classe UneClasse.
$bouchon = $this->getMock('UneClasse');
// Configurer le bouchon.
$bouchon->expects($this->any())
->method('faireQuelquechose')
->will($this->returnSelf());
// $bouchon->faireQuelquechose() retourne $bouchon
$this->assertSame($bouchon, $bouchon->faireQuelquechose());
}
}
?>
Parfois, une méthode bouchon doit retourner différentes valeurs selon
une liste prédéfinie d'arguments. Vous pouvez utiliser
returnValueMap() pour créer un mappage qui associe des
paramètres aux valeurs de retour correspondantes. Voir
Exemple 10.6, « Bouchonner un appel de méthode pour retourner la valeur à partir d'un mappage » pour
un exemple.
Exemple 10.6. Bouchonner un appel de méthode pour retourner la valeur à partir d'un mappage
<?php
require_once 'UneClasse.php';
class BouchonTest extends PHPUnit_Framework_TestCase
{
public function testReturnValueMapBouchon()
{
// Créer un bouchon pour la classe UneClasse.
$bouchon = $this->getMock('UneClasse');
// Créer un mappage des arguments
// et des valeurs de retour.
$map = array(
array('a', 'b', 'c', 'd'),
array('e', 'f', 'g', 'h')
);
// Configurer le bouchon.
$bouchon->expects($this->any())
->method('faireQuelquechose')
->will($this->returnValueMap($map));
// $bouchon->faireQuelquechose() retourne
// différentes valeurs selon les paramètres
// fournis.
$this->assertEquals('d', $bouchon->faireQuelquechose('a', 'b', 'c'));
$this->assertEquals('h', $bouchon->faireQuelquechose('e', 'f', 'g'));
}
}
?>
Quand l'appel méthode bouchonné doit retourner une valeur calculée au lieu
d'une valeur fixée (voir returnValue()) ou un paramètre
(non modifié) (voir returnArgument()), vous pouvez utiliser
returnCallback() pour que la méthode retourne le résultat
d'une fonction ou méthode de rappel. Voir
Exemple 10.7, « Bouchonner un appel de méthode pour retourner une valeur à partir d'un rappel » pour un exemple.
Exemple 10.7. Bouchonner un appel de méthode pour retourner une valeur à partir d'un rappel
<?php
require_once 'UneClasse.php';
class BouchonTest extends PHPUnit_Framework_TestCase
{
public function testReturnCallbackBouchon()
{
// Créer un bouchon pour la classe UneClasse.
$bouchon = $this->getMock('UneClasse');
// Configurer le bouchon.
$bouchon->expects($this->any())
->method('faireQuelquechose')
->will($this->returnCallback('str_rot13'));
// $bouchon->faireQuelquechose($argument) retourne str_rot13($argument)
$this->assertEquals('fbzrguvat', $bouchon->faireQuelquechose('quelqueChose'));
}
}
?>
Une alternative plus simple pour configurer une méthode de rappel peut
consister à indiquer une liste de valeurs désirées. Vous pouvez faire
ceci avec la méthode onConsecutiveCalls(). Voir
Exemple 10.8, « Bouchonner un appel de méthode pour retourner une liste de valeurs dans l'ordre indiqué » pour
un exemple.
Exemple 10.8. Bouchonner un appel de méthode pour retourner une liste de valeurs dans l'ordre indiqué
<?php
require_once 'UneClasse.php';
class BouchonTest extends PHPUnit_Framework_TestCase
{
public function testOnConsecutiveCallsBouchon()
{
// Créer un bouchon pour la classe UneClasse.
$bouchon = $this->getMock('UneClasse');
// Configurer le bouchon.
$bouchon->expects($this->any())
->method('faireQuelquechose')
->will($this->onConsecutiveCalls(2, 3, 5, 7));
// $bouchon->faireQuelquechose() retourne une valeur différente à chaque fois
$this->assertEquals(2, $bouchon->faireQuelquechose());
$this->assertEquals(3, $bouchon->faireQuelquechose());
$this->assertEquals(5, $bouchon->faireQuelquechose());
}
}
?>
Au lieu de retourner une valeur, une méthode bouchon peut également lever
une exception. Exemple 10.9, « Bouchonner un appel de méthode pour lever une exception »
montre comme utiliser throwException() pour faire cela.
Exemple 10.9. Bouchonner un appel de méthode pour lever une exception
<?php
require_once 'UneClasse.php';
class BouchonTest extends PHPUnit_Framework_TestCase
{
public function testThrowExceptionBouchon()
{
// Créer un bouchon pour la classe UneClasse.
$bouchon = $this->getMock('UneClasse');
// Configurer le bouchon.
$bouchon->expects($this->any())
->method('faireQuelquechose')
->will($this->throwException(new Exception));
// $bouchon->faireQuelquechose() lance l'Exception
$bouchon->faireQuelquechose();
}
}
?>
Alternativement, vous pouvez écrire le bouchon vous-même et améliorer
votre conception ce-faisant. Des ressources largement utilisées sont
accédées via une unique façade, de telle sorte que vous pouvez facilement
remplacer la ressource avec le bouchon. Par exemple, au lieu d'avoir
des appels directs à la base de données éparpillés dans tout le code,
vous avez un unique objet Database, une implémentation de
l'interface IDatabase. Ensuite, vous pouvez créer
une implémentation bouchon de IDatabase et l'utiliser pour
vos tests. Vous pouvez même créer une option pour lancer les tests dans la
base de données bouchon ou la base de données réelle, de telle sorte que vous
pouvez utiliser vos tests à la fois pour tester localement pendant le développement
et en intégration avec la vraie base de données.
Les fonctionnalités qui nécessitent d'être bouchonnées tendent à se regrouper dans le même objet, améliorant la cohésion. En représentant la fonctionnalité avec une unique interface cohérente, vous réduisez le couplage avec le reste du système.
La pratique consistant à remplacer un objet avec une doublure de test qui vérifie des attentes, par exemple en faisant l'assertion qu'une méthode a été appelée, est appelée simulacre.
Vous pouvez utiliser un objet simulacre "comme un point d'observation qui est utilisé pour vérifier les sorties indirectes du système quand il est testé. Typiquement, le simulacre inclut également la fonctionnalité d'un bouchon de test, en ce sens qu'il doit retourner les valeurs du système testé s'il n'a pas déjà fait échouer les tests mais l'accent est mis sur la vérification des sorties indirectes. Ainsi, un simulacre est un beaucoup plus qu'un simple bouchon avec des assertions; il est utilisé d'une manière fondamentalement différente".
Voici un exemple: supposons que vous voulez tester que la méthode correcte,
update() dans notre exemple, est appelée d'un objet qui observe un autre objet.
Exemple 10.10, « Les classes Sujet et Observateur qui sont une partie du système testé »
illustre le code pour les classes Sujet et Observateur
qui sont une partie du système testé (SUT).
Exemple 10.10. Les classes Sujet et Observateur qui sont une partie du système testé
<?php
class Sujet
{
protected $observateurs = array();
public function attache(Observateur $observateur)
{
$this->observateurs[] = $observateur;
}
public function faireQuelquechose()
{
// Faire quelque chose.
// ...
// Avertir les observateurs que nous faisons quelque chose.
$this->notify('quelque chose');
}
public function faireQuelquechoseMal()
{
foreach ($this->observateurs as $observateur) {
$observateur->reportError(42, 'Quelque chose de mal est arrivé', $this);
}
}
protected function notify($paramètre)
{
foreach ($this->observateurs as $observateur) {
$observateur->update($paramètre);
}
}
// Autres méthodes.
}
class Observateur
{
public function update($paramètre)
{
// Faire quelque chose.
}
public function reportError($codeErreur, $messageErreur, Sujet $sujet)
{
// Faire quelque chose
}
// Autres méthodes.
}
?>
Exemple 10.11, « Tester qu'une méthode est appelée une fois et avec un paramètre indiqué »
illustre comment utiliser un simulacre pour tester l'interaction entre
les objets Sujet et Observateur.
Nous utilisons d'abord la méthode getMock() qui est fournie par
la classe PHPUnit_Framework_TestCase pour configurer un simulacre
pour l'Observateur. Puisque nous donnons un tableau comme second
paramètre (facultatif) pour la méthode getMock(),
seule la méthode update() de la classe Observateur est
remplacée par une implémentation d'un simulacre.
Exemple 10.11. Tester qu'une méthode est appelée une fois et avec un paramètre indiqué
<?php
require_once 'Sujet.php';
class SujetTest extends PHPUnit_Framework_TestCase
{
public function testLesObservateursSontMisAJour()
{
// Créer un simulacre pour la classe Observateur,
// ne touchant que la méthode update().
$observateur = $this->getMock('Observateur', array('update'));
// Configurer l'attente de la méthode update()
// d'être appelée une seule fois et avec la chaîne 'quelquechose'
// comme paramètre.
$observateur->expects($this->once())
->method('update')
->with($this->equalTo('quelque chose'));
// Créer un objet Sujet et y attacher l'objet Observateur
// simulé
$sujet = new Sujet;
$sujet->attache($observateur);
// Appeler la méthode faireQuelquechose() sur l'objet $sujet
// que nous attendons voir appeler la méthode update() de l'objet
// simulé Observateur avec la chaîne 'quelqueChose'.
$sujet->faireQuelquechose();
}
}
?>
La méthode with() peut prendre n'importe quel
nombre de paramètres, correspondant au nombre de paramètres des méthodes
étant simulées. Vous pouvez indiquer des contraintes plus avancées
sur les paramètres de méthode qu'une simple correspondance.
Exemple 10.12. Tester qu'une méthode est appelée avec un nombre de paramètres contraints de différentes manières
<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
public function testRapportErreur()
{
// Créer un simulacre pour la classe Observateur, en simulant
// la méthode rapportErreur()
$observateur = $this->getMock('Observateur', array('rapportErreur'));
$observateur->expects($this->once())
->method('rapportErreur')
->with($this->greaterThan(0),
$this->stringContains('Quelquechose'),
$this->anything());
$sujet = new Subject;
$sujet->attach($observateur);
// La méthode faireQuelquechoseDeMal doit rapporter une erreur à l'observateur
// via la méthode rapportErreur()
$sujet->faireQuelquechoseDeMal();
}
}
?>
Tableau 4.3, « Contraintes » montre les contraintes qui peuvent être appliquées aux paramètres de méthode et Tableau 10.1, « Matchers » montre les matchers qui sont disponibles pour indiquer le nombre d' invocations.
Tableau 10.1. Matchers
| Matcher | Signification |
|---|---|
PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount any() | Retourne un matcher qui correspond quand la méthode pour laquelle il est évalué est exécutée zéro ou davantage de fois. |
PHPUnit_Framework_MockObject_Matcher_InvokedCount never() | Retourne un matcher qui correspond quand la méthode pour laquelle il est évalué n'est jamais exécutée. |
PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce atLeastOnce() | Retourne un matcher qui correspond quand la méthode pour laquelle il est évalué est exécutée au moins une fois. |
PHPUnit_Framework_MockObject_Matcher_InvokedCount once() | Retourne un matcher qui correspond quand la méthode pour laquelle il est évalué est exécutée exactement une fois. |
PHPUnit_Framework_MockObject_Matcher_InvokedCount exactly(int $nombre) | Retourne un matcher qui correspond quand la méthode pour laquelle il est évalué est exécutée exactement $nombre fois. |
PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex at(int $index) | Retourne un matcher qui correspond quand la méthode pour laquelle il est évalué est invoquée pour l'$index spécifié. |
La méthode getMockForAbstractClass() retourne un simulacre
pour une classe abstraite. Toutes les méthodes abstraites d'une classe simulacre
donnée sont simulées. Ceci permet de tester les méthodes concrètes d'une classe
abstraite.
Exemple 10.13. Tester les méthodes concrêtes d'une classe abstraite
<?php
abstract class ClasseAbstraite
{
public function methodeConcrete()
{
return $this->methodeAbstraite();
}
public abstract function methodeAbstraite();
}
class ClasseAbstraiteTest extends PHPUnit_Framework_TestCase
{
public function testConcreteMethod()
{
$stub = $this->getMockForAbstractClass('ClasseAbstraite');
$stub->expects($this->any())
->method('methodeAbstraite')
->will($this->returnValue(TRUE));
$this->assertTrue($stub->methodeConcrete());
}
}
?>
Quand votre application interagit avec un web service, vous voulez le
tester sans vraiment interagir avec le web service. Pour rendre facile
la création de bouchon ou de simulacre de web services,
getMockFromWsdl() peut être utilisée de la même façon que
getMock() (voir plus haut). La seule différence est que
getMockFromWsdl() retourne un bouchon ou un simulacre
basé sur la description en WSDL d'un web service tandis que getMock()
retourne un bouchon ou un simulacre basé sur une classe ou une interface PHP.
Exemple 10.14, « Bouchonner un web service »
montre comment getMockFromWsdl() peut être utilisé pour faire un bouchon,
par exemple, d'un web service décrit dans GoogleSearch.wsdl.
Exemple 10.14. Bouchonner un web service
<?php
class GoogleTest extends PHPUnit_Framework_TestCase
{
public function testSearch()
{
$googleSearch = $this->getMockFromWsdl(
'GoogleSearch.wsdl', 'GoogleSearch'
);
$directoryCategory = new StdClass;
$directoryCategory->fullViewableName = '';
$directoryCategory->specialEncoding = '';
$element = new StdClass;
$element->summary = '';
$element->URL = 'http://www.phpunit.de/';
$element->snippet = '...';
$element->title = '<b>PHPUnit</b>';
$element->cachedSize = '11k';
$element->relatedInformationPresent = TRUE;
$element->hostName = 'www.phpunit.de';
$element->directoryCategory = $directoryCategory;
$element->directoryTitle = '';
$result = new StdClass;
$result->documentFiltering = FALSE;
$result->searchComments = '';
$result->estimatedTotalResultsCount = 378000;
$result->estimateIsExact = FALSE;
$result->resultElements = array($element);
$result->searchQuery = 'PHPUnit';
$result->startIndex = 1;
$result->endIndex = 1;
$result->searchTips = '';
$result->directoryCategories = array();
$result->searchTime = 0.248822;
$googleSearch->expects($this->any())
->method('doGoogleSearch')
->will($this->returnValue($result));
/**
* $googleSearch->doGoogleSearch() va maintenant retourner un result bouchon et
* la méthode doGoogleSearch() du web service ne sera pas invoquée.
*/
$this->assertEquals(
$result,
$googleSearch->doGoogleSearch(
'00000000000000000000000000000000',
'PHPUnit',
0,
1,
FALSE,
'',
FALSE,
'',
'',
''
)
);
}
}
?>
vfsStream est un encapsuleur de flux pour un système de fichiers virtuel qui peut s'avérer utile dans des tests unitaires pour simuler le vrai système de fichiers.
Pour installer vfsStream, le canal PEAR
(pear.php-tools.net) qui est utilisé pour
sa distribution doit être enregistré dans l'environnement local PEAR:
pear channel-discover pear.php-tools.netCeci ne doit être fait qu'une seule fois. Maintenant, l'installeur PEAR peut être utilisé pour installer vfsStream.
pear install pat/vfsStream-betaExemple 10.15, « Une classe qui interagit avec le système de fichiers » montre une classe qui interagit avec le système de fichiers.
Exemple 10.15. Une classe qui interagit avec le système de fichiers
<?php
class Exemple
{
protected $id;
protected $repertoire;
public function __construct($id)
{
$this->id = $id;
}
public function setRepertoire($repertoire)
{
$this->repertoire = $repertoire . DIRECTORY_SEPARATOR . $this->id;
if (!file_exists($this->repertoire)) {
mkdir($this->repertoire, 0700, TRUE);
}
}
}?>
Sans un système de fichiers virtuel tel que vfsStream, nous ne pouvons
pas tester la méthode setDirectory() en isolation des influences
extérieures (voir Exemple 10.16, « Tester une classe qui interagoit avec le système de fichiers »).
Exemple 10.16. Tester une classe qui interagoit avec le système de fichiers
<?php
require_once 'Exemple.php';
class ExempleTest extends PHPUnit_Framework_TestCase
{
protected function setUp()
{
if (file_exists(dirname(__FILE__) . '/id')) {
rmdir(dirname(__FILE__) . '/id');
}
}
public function testReprtoireEstCree()
{
$example = new Exemple('id');
$this->assertFalse(file_exists(dirname(__FILE__) . '/id'));
$example->setRepertoire(dirname(__FILE__));
$this->assertTrue(file_exists(dirname(__FILE__) . '/id'));
}
protected function tearDown()
{
if (file_exists(dirname(__FILE__) . '/id')) {
rmdir(dirname(__FILE__) . '/id');
}
}
}
?>
L'approche précédente possède plusieurs inconvénients :
Comme avec les ressources externes, il peut y a voir des problèmes intermittents avec le système de fichiers. Ceci rend les tests qui interagissent avec lui peu fiables.
Dans les méthodes setUp() et tearDown(), nous avons à nous assurer que le répertoire n'existe pas avant et après le test.
Si l'exécution du test s'achève avant que la méthode tearDown() n'ait été appelée, le répertoire va rester dans le système de fichiers.
Exemple 10.17, « Simuler le système de fichiers dans un test pour une classe qui interagit avec le système de fichiers » montre comment vfsStream peut être utilisé pour simuler le système de fichiers dans un test pour une classe qui interagit avec le système de fichiers.
Exemple 10.17. Simuler le système de fichiers dans un test pour une classe qui interagit avec le système de fichiers
<?php
require_once 'vfsStream/vfsStream.php';
require_once 'Exemple.php';
class ExempleTest extends PHPUnit_Framework_TestCase
{
public function setUp()
{
vfsStreamWrapper::register();
vfsStreamWrapper::setRoot(new vfsStreamDirectory('exempleRepertoire'));
}
public function testRepertoireEstCree()
{
$exemple = new Exemple('id');
$this->assertFalse(vfsStreamWrapper::getRoot()->hasChild('id'));
$exemple->setRepertoire(vfsStream::url('exempleRepertoire'));
$this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('id'));
}
}
?>
Ceci présente plusieurs avantages :
Le test lui-même est plus concis.
vfsStream donne au développeur du test le plein contrôle sur la façon dont le code testé voit l'environnement du système de fichiers.
Puisque les opérations du système de fichiers n'opèrent plus sur le système de fichiers réel, les opérations de nettoyage dans la méthode tearDown() ne sont plus nécessaires.
Vous pouvez toujours écrire davantage de tests. Cependant, vous vous rendrez rapidement compte que seule une fraction des tests auxquels vous pensez est réellement utile. Ce que vous voulez, c'est écrire des tests qui échouent même si vous pensez qu'ils devraient fonctionner, ou des tests qui réussissent même si vous pensez qu'ils devraient échouer. Une autre façon d'envisager cela consiste à le faire en terme de coûts/bénéfices. Vous voulez écrire des tests qui vous paient en retour avec des informations. | ||
| --Erich Gamma | ||
Lorsque vous avez besoin de modifier la structure interne d'un logiciel sur lequel vous êtes en train de travailler pour la rendre plus facile à comprendre et moins chère à modifier sans modifier son comportement visible, une suite de tests est inestimable pour appliquer de façon sure ces refactorings comme on les appelle. Sans cela, vous ne pourrez pas vous détecter que vous avez cassé le système en réalisant la restructuration.
Les conditions suivantes vous aideront à améliorer le code et la conception de votre projet, tant que vous utiliserez des tests unitaires pour vérifier que les étapes de transformation dues au refactoring préservent bien le comportement et n'introduisent pas d'erreurs :
Tous les tests unitaires fonctionnent correctement.
Le code transmet des principes de conception.
Le code ne contient pas de redondances.
Le code contient le nombre minimal de classes et de méthodes.
Quand vous avez besoin d'ajouter de nouvelles fonctionnalités au système, écrivez d'abord les tests. Puis, vous aurez terminé quand les tests s'exécuteront. Cette pratique sera discutée en détail dans le prochain chapitre.
Quand vous recevez un rapport de bug, vous pourriez être tenté de le corriger aussi vite que possible. L'expérience prouve que cette impulsion ne vous rendra pas service : il est probable que ce défaut en provoque un autre.
Vous pouvez maîtriser cette impulsion en suivant ce qui suit :
Vérifiez que vous pouvez reproduire le défaut.
Trouvez la démonstration la plus ciblée possible du défaut dans le code. Par exemple, si un nombre s'affiche incorrectement dans une sortie écran, trouvez l'objet qui calcule ce nombre.
Ecrivez un test automatisé qui échoue maintenant mais qui réussira quand le défaut sera corrigé.
Corrigez le défaut.
Trouver le moyen fiable le plus ciblé pour reproduire le défaut vous offre l'opportunité de vraiment en examiner la cause. Le test que vous écrivez va améliorer les chances que lorsque vous corrigerez le défaut, vous le fassiez vraiment du fait que le nouveau test réduit la probabilité de défaire cette correction lors de futures modifications du code. Tous les tests écrits avant réduisent la probabilité de causer par inadvertance un problème différent.
Le test unitaire offre de nombreux avantages :
Globalement, le test unitaire intégré minimise les coûts et les risques de toute modification individuelle. Il permettra au projet de mener [...] des améliorations majeures d'architecture [...] rapidement et avec confiance. | ||
| --Benjamin Smedberg | ||
Les tests unitaires sont une partie vitale pour plusieurs pratiques et processus de développement logiciel tel que la programmation en testant d'abord, l'Extreme Programming, et le développement dirigé par les tests. Ils permettent également la conception par contrat dans des langages de programmation qui ne supportent pas cette méthodologie par construction de langage.
Vous pouvez utiliser PHPUnit pour écrire des tests une fois que vous avez fait la programmation. Cependant, plus tôt un test est écrit après qu'une erreur a été introduite, plus le test a de la valeur. Ainsi, au lieu d'écrire des tests des mois après que le code est "achevé", nous pouvons écrire des tests quelques jours, heures ou minutes après la possible introduction d'un défaut. Pourquoi s'arrêter là ? Pourquoi ne pas écrire les tests un peu avant la possible introduction d'un défaut ?
La programmation en testant d'abord, qui est une partie de l'Extreme Programming et le développement dirigé par les tests, sont bâtis sur cette idée et la poussent à l'extrême. Grâce à la puissance de calcul actuelle, nous avons l'opportunité de lancer des milliers de tests des milliers de fois par jour. Nous pouvons utiliser les retours de tous ces tests pour programmer par petites étapes, chacune d'elles apportant avec elle l'assurance d'un nouveau test automatisé s'ajoutant à tous les tests venus précédemment. Les tests sont comme des pitons, vous assurant que, quoi qu'il arrive, une fois que vous avez progressé, vous ne pouvez pas retomber plus bas.
Quand vous écrivez le test la première fois, il ne peut pas être exécuté, car vous faites appel à des objets et des méthodes qui n'ont pas encore été programmés. Ceci peut sembler étrange au premier abord, mais après un moment, vous aurez l'habitude de procéder ainsi. Pensez à la programmation en testant d'abord comme à une approche pragmatique pour suivre le principe de programmation orientée objet consistant à programmer une interface au lieu de programmer une implémentation : quand vous écrivez le test, vous pensez à l'interface de l'objet que vous êtes en train de tester - ce à quoi ressemble cet objet vu de l'extérieur. Quand vous faites en sorte que le test fonctionne vraiment, vous réfléchissez en terme de pure implémentation. L'interface est déterminée par le test en échec.
L'objet du Développement dirigé par les tests est de rechercher les fonctionnalités dont le logiciel a réellement besoin, plutôt que celles dont le programmeur pense qu'il pourrait probablement avoir besoin. La façon dont il procède semble d'abord contre intuitive, si ce n'est carrément idiote, mais non seulement cela a du sens, mais cela devient également rapidement une façon naturelle et élégante de développer du logiciel. | ||
| --Dan North | ||
Ce qui suit est forcément une introduction abrégée au développement dirigé par les tests. Vous pouvez approfondir le sujet dans d'autres livres, comme Test-Driven Development [Beck2002] de Kent Beck ou A Practical Guide to Test-Driven Development [Astels2003] de Dave Astels.
Dans cette section, nous examinerons l'exemple d'une classe qui représente
un compte bancaire. Le contrat pour la classe CompteBancaire
n'exige pas seulement des méthodes pour obtenir et positionner la balance
du compte bancaire, ainsi que des méthodes pour déposer et retirer de l'argent.
S'y ajoute les deux conditions suivantes qui doivent être vérifiées :
La balance initiale du compte bancaire doit être à zéro.
La balance du compte bancaire ne peut pas devenir négative.
Nous écrivons les tests pour la classe CompteBancaire avant d'écrire
le code de la classe elle-même. Nous utilisons les conditions du contrat comme base pour les
tests et nous nommons les méthodes de test en fonction, comme montré dans
Exemple 12.1, « Tests pour la classe CompteBancaire ».
Exemple 12.1. Tests pour la classe CompteBancaire
<?php
require_once 'CompteBancaire.php';
class CompteBancaireTest extends PHPUnit_Framework_TestCase
{
protected $compte_bancaire;
protected function setUp()
{
$this->compte_bancaire = new CompteBancaire;
}
public function testBalanceEstInitialementAZero()
{
$this->assertEquals(0, $this->compte_bancaire->getBalance());
}
public function testBalanceNePeutPasEtreNegatif()
{
try {
$this->compte_bancaire->retirerArgent(1);
}
catch (CompteBancaireException $e) {
$this->assertEquals(0, $this->compte_bancaire->getBalance());
return;
}
$this->fail();
}
public function testBalanceNePeutPasEtreNegatif2()
{
try {
$this->compte_bancaire->deposerArgent(-1);
}
catch (CompteBancaireException $e) {
$this->assertEquals(0, $this->compte_bancaire->getBalance());
return;
}
$this->fail();
}
}
?>
Nous écrivons maintenant le volume minimum de code pour que le premier test,
testBalanceEstInitialementAZero(), réussisse. Dans notre
exemple, ceci correspond à implémenter la méthode getBalance()
de la classe CompteBancaire, comme montré dans
Exemple 12.2, « Code nécessaire pour que le test testBalanceEstInitialementAZero() réussisse ».
Exemple 12.2. Code nécessaire pour que le test testBalanceEstInitialementAZero() réussisse
<?php
class CompteBancaire
{
protected $balance = 0;
public function getBalance()
{
return $this->balance;
}
}
?>
Maintenant, le test pour la première condition du contrat réussit, mais les tests pour la seconde condition du contrat échoue car nous n'avons pas implémenté les méthodes que ces tests appellent.
phpunit CompteBancaireTest
PHPUnit 3.6.0 by Sebastian Bergmann.
.
Fatal error: Call to undefined method CompteBancaire::retirerArgent()
Pour que les tests qui s'assurent que la seconde condition du contrat réussissent,
nous devons maintenant implémenter les méthodes retirerArgent(),
deposerArgent() et setBalance(),
comme montré dans
Exemple 12.3, « La classe CompteBancaire complète ».
Ces méthodes sont écrites de telle façon qu'elles lèvent une
CompteBancaireException quand elles sont appelées avec des valeurs illégales
qui violeraient les conditions du contrat.
Exemple 12.3. La classe CompteBancaire complète
<?php
class CompteBancaireException extends Exception { }
class CompteBancaire
{
protected $balance = 0;
public function getBalance()
{
return $this->balance;
}
protected function setBalance($balance)
{
if ($balance >= 0) {
$this->balance = $balance;
} else {
throw new CompteBancaireException;
}
}
public function deposerArgent($balance)
{
$this->setBalance($this->getBalance() + $balance);
return $this->getBalance();
}
public function retirerArgent($balance)
{
$this->setBalance($this->getBalance() - $balance);
return $this->getBalance();
}
}
?>
Les tests qui assurent que la seconde condition du contrat réussissent maintenant aussi :
phpunit CompteBancaireTest
PHPUnit 3.6.0 by Sebastian Bergmann.
...
Time: 0 seconds
OK (3 tests, 3 assertions)
Alternativement, vous pouvez utiliser les méthodes statiques de vérification
fournies par la classe PHPUnit_Framework_Assert pour écrire les conditions
du contrat en tant que vérification en style "conception par contrat", comme montré dans
Exemple 12.4, « La classe CompteBancaire avec des vérifications de conception par contrat ».
Quand l'une de ces vérifications échoue, une exception
PHPUnit_Framework_AssertionFailedError sera levée.
Avec cette approche, vous écrivez moins de code pour les contrôles des conditions du contrat
et les tests deviennent plus lisibles. Cependant, vous ajoutez une dépendance à l'exécution
avec PHPUnit à vos projets.
Exemple 12.4. La classe CompteBancaire avec des vérifications de conception par contrat
<?php
class CompteBancaire
{
private $balance = 0;
public function getBalance()
{
return $this->balance;
}
protected function setBalance($balance)
{
PHPUnit_Framework_Assert::assertTrue($balance >= 0);
$this->balance = $balance;
}
public function deposerArgent($montant)
{
PHPUnit_Framework_Assert::assertTrue($montant >= 0);
$this->setBalance($this->getBalance() + $montant);
return $this->getBalance();
}
public function retirerArgent($montant)
{
PHPUnit_Framework_Assert::assertTrue($montant >= 0);
PHPUnit_Framework_Assert::assertTrue($this->balance >= $montant);
$this->setBalance($this->getBalance() - $montant);
return $this->getBalance();
}
}
?>
En écrivant les conditions du contrat dans les tests, nous avons utilisé
la conception par contrat pour programmer la classe CompteBancaire.
Nous avons alors écrit, suivant l'approche de la programmation en testant d'abord, le code
nécessaire pour faire que les tests réussissent. Cependant, nous avons oublié d'écrire
les tests qui appellent setBalance(),
deposerArgent() et retirerArgent()
avec des valeurs valides qui ne violent pas les conditions du contrat.
Nous avons besoin d'un moyen pour tester nos tests, ou au moins mesurer leur qualité.
Un tel moyen est l'analyse de l'information de couverture de code que nous allons voir.
Dans [Astels2006], Dave Astels fait le point suivant :
Extreme Programming avait à l'origine comme règle de tester tout ce qui peut éventuellement provoquer un défaut.
Maintenant, cependant, la pratique du test en Extreme Programming a évolué en Développement dirigé par les tests (voir Chapitre 12, Développement dirigé par les tests).
Mais les outils continuent à obliger les développeurs à penser en terme de tests et de vérifications au lieu de spécifications.
Donc, s'il ne s'agit pas de tester, de quoi s'agit-il ? Il s'agit de s'imaginer ce que vous essayez de faire avant de partir en court de route pour essayer de le faire. Vous écrivez une spécification qui fixe une petite partie du comportement sous une forme concise, non ambiguë et exécutable. C'est aussi simple que ça. Cela signifie-t'il que vous écrivez des tests ? Non. Cela signifie que vous écrivez des spécifications sur ce que votre code a à faire. Cela signifie que vous spécifiez le comportement de votre code dans le temps. Mais pas trop loin dans le temps. En fait, juste avant d'écrire le code, c'est mieux car c'est quand vous avez autant d'information que vous voulez sous la main à ce moment. Comme avec le développement dirigé par les tests quand il est bien fait, vous travaillez par petits incréments... en spécifiant un petit aspect du comportement à la fois, puis vous l'implémentez. Quand vous réalisez qu'il s'agit de spécifier un comportement et pas écrire des tests, votre point de vue se déplace. Soudain, l'idée d'avoir une classe de test pour chacune de vos classes de production est ridiculement limitant. Et la pensée de tester chacune de vos méthodes avec leurs propres méthodes de test (dans une relation 1 pour 1) sera risible. | ||
| --Dave Astels | ||
L'accent mis par le Développement dirigé par le comportement est sur "le langage et les interactions utilisés dans le processus du développement logiciel. Les développeurs dirigés par le comportement utilisent leur langue naturelle en combinaison avec le langage polyvalent de la conception dirigée par le domaine pour décrire le but et le bénéfice de leur code. Ceci permet aux développeurs de se concentrer sur pourquoi le code doit être créé, plutôt que sur les détails techniques, et de minimiser les traductions entre le langage technique dans lequel le code est écrit et le langage du domaine parlé par les "experts du domaine".
La classe PHPUnit_Extensions_Story_TestCase ajouter un framework d'histoire (story)
qui facilite la définition d'un
Langage spécifique au domaine
pour le développement dirigé par le comportement. Il peut être installé comme ceci :
pear install phpunit/PHPUnit_Story
A l'intérieur d'un scénario, given(),
when() et then() représentent chacun une
étape. and() est du même type que les
étapes précédentes. Les méthodes suivantes sont déclarées abstract
dans PHPUnit_Extensions_Story_TestCase et doivent être implémentées :
runGiven(&$monde, $action, $parametres)
...
runWhen(&$monde, $action, $parametres)
...
runThen(&$monde, $action, $parametres)
...
Dans cette section, nous examinerons l'exemple d'une classe qui calcule le score d'un jeu de bowling. Les règles de ce jeu sont les suivantes :
Le jeu comprend 10 manches.
Dans chaque manche, le joueur a deux possibilités de faire tomber 10 quilles.
Le score pour une manche est le nombre total de quilles tombées, plus des bonus pour les strikes et les spares.
Un spare, c'est quand le joueur fait tomber les 10 quilles en 2 essais.
Le bonus pour ce type de manche est le nombre de quilles renversées lors du lancer suivant.
Un strike, c'est quand le joueur fait tomber les 10 quilles à son premier essai.
Le bonus pour ce type de manche est le nombre de quilles renversées lors des deux lancers suivants.
Exemple 13.1, « Spécification pour la classe JeuDeBowling »
montre comment les règles précédentes sont écrites comme scénarios de spécification en utilisant
PHPUnit_Extensions_Story_TestCase.
Exemple 13.1. Spécification pour la classe JeuDeBowling
<?php
require_once 'PHPUnit/Extensions/Story/TestCase.php';
require_once 'JeuDeBowling.php';
class JeuDeBowlingSpec extends PHPUnit_Extensions_Story_TestCase
{
/**
* @scenario
*/
public function scorePourJeuDansLaGoutiereEst0()
{
$this->given('Nouvelle partie')
->then('Le score doit être', 0);
}
/**
* @scenario
*/
public function scorePourToutDUnSeulCoupEst20()
{
$this->given('Nouvelle partie')
->when('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->and('Le joueur lance et renverse', 1)
->then('Le score doit être', 20);
}
/**
* @scenario
*/
public function scorePourUnSpareEt3Est16()
{
$this->given('Nouvelle partie')
->when('Le joueur lance et renverse', 5)
->and('Le joueur lance et renverse', 5)
->and('Le joueur lance et renverse', 3)
->then('Le score doit être', 16);
}
/**
* @scenario
*/
public function scorePourUnStrikeEt3Est24()
{
$this->given('Nouvelle partie')
->when('Le joueur lance et renverse', 10)
->and('Le joueur lance et renverse', 3)
->and('Le joueur lance et renverse', 4)
->then('Le score doit être', 24);
}
/**
* @scenario
*/
public function scorePourUnJeuParfaitEst300()
{
$this->given('Nouvelle partie')
->when('Le joueur lance et renverse', 10)
->and('Le joueur lance et renverse', 10)
->and('Le joueur lance et renverse', 10)
->and('Le joueur lance et renverse', 10)
->and('Le joueur lance et renverse', 10)
->and('Le joueur lance et renverse', 10)
->and('Le joueur lance et renverse', 10)
->and('Le joueur lance et renverse', 10)
->and('Le joueur lance et renverse', 10)
->and('Le joueur lance et renverse', 10)
->and('Le joueur lance et renverse', 10)
->and('Le joueur lance et renverse', 10)
->then('Le score doit être', 300);
}
public function runGiven(&$monde, $action, $parametres)
{
switch($action) {
case 'Nouvelle partie': {
$monde['jeu'] = new JeuDeBowling;
$monde['lancers'] = 0;
}
break;
default: {
return $this->notImplemented($action);
}
}
}
public function runWhen(&$monde, $action, $parametres)
{
switch($action) {
case 'Le joueur lance et renverse': {
$monde['jeu']->lancerEtRenverser($parametres[0]);
$monde['lancers']++;
}
break;
default: {
return $this->notImplemented($action);
}
}
}
public function runThen(&$monde, $action, $parametres)
{
switch($action) {
case 'Le score doit être': {
for ($i = $monde['lancers']; $i < 20; $i++) {
$monde['jeu']->lancerEtRenverser(0);
}
$this->assertEquals($parametres[0], $monde['jeu']->score());
}
break;
default: {
return $this->notImplemented($action);
}
}
}
}
?>
phpunit --printer PHPUnit_Extensions_Story_ResultPrinter_Text JeuDeBowlingSpec
PHPUnit 3.6.0 by Sebastian Bergmann.
JeuDeBowlingSpec
[x] Score pour jeu dans la goutiere est 0
Given Nouvelle partie
Then Le score doit être 0
[x] Score pour tout d un seul coup est 20
Given Nouvelle partie
When Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
and Le joueur lance et renverse 1
Then Le score doit être 20
[x] Score pour un spare et 3 est 16
Given Nouvelle partie
When Le joueur lance et renverse 5
and Le joueur lance et renverse 5
and Le joueur lance et renverse 3
Then Le score doit être 16
[x] Score pour un strike et 3 est 24
Given Nouvelle partie
When Le joueur lance et renverse 10
and Le joueur lance et renverse 3
and Le joueur lance et renverse 4
Then Le score doit être 24
[x] Score pour un jeu parfait est 300
Given Nouvelle partie
When Le joueur lance et renverse 10
and Le joueur lance et renverse 10
and Le joueur lance et renverse 10
and Le joueur lance et renverse 10
and Le joueur lance et renverse 10
and Le joueur lance et renverse 10
and Le joueur lance et renverse 10
and Le joueur lance et renverse 10
and Le joueur lance et renverse 10
and Le joueur lance et renverse 10
and Le joueur lance et renverse 10
and Le joueur lance et renverse 10
Then Le score doit être 300
Scenarios: 5, Failed: 0, Skipped: 0, Incomplete: 0.La beauté du test ne se trouve pas dans l'effort mais dans l'efficience. Savoir ce qui doit être testé est magnifique, et savoir ce qui est testé est magnifique. | ||
| --Murali Nandigama | ||
Dans ce chapitre, vous apprendrez tout sur la fonctionnalité de couverture de code de PHPUnit qui fournit une vision interne des parties du code de production qui sont exécutées quand les tests sont exécutés. Cela aide à répondre à des questions comme :
Comment trouvez-vous le code qui n'est pas encore testé - ou, en d'autres mots, pas encore couvert par un test ?
Comment mesurez-vous la complétude du test ?
Un exemple de ce que peuvent signifier des statistiques de couverture de code est, s'il y a une méthode avec 100 lignes de code, et seulement 75 de ces lignes sont réellement exécutées quand les tests sont lancés, alors la méthode est considérée comme ayant une couverture de code de 75 pour cent.
La fonctionnalité de couverture de code de PHPUnit fait usage du composant PHP_CodeCoverage qui, à son tour, tire partie de la fonctionnalité de couverture d'instructions fournie par l'extension Xdebug de PHP.
Générons un rapport de couverture de code pour la classe CompteBancaire
de Exemple 12.3, « La classe CompteBancaire complète ».
phpunit --coverage-html ./rapport CompteBancaireTest
PHPUnit 3.6.0 by Sebastian Bergmann.
...
Time: 0 seconds
OK (3 tests, 3 assertions)
Generating report, this may take a moment.
Figure 14.1, « Couverture de code pour setBalance() » montre
un extrait du rapport de couverture de code. Les lignes de code qui ont été
exécutés pendant le fonctionnement des tests sont surlignés en vert, les lignes
de code qui sont exécutables mais n'ont pas été exécutées sont surlignées en rouge
et le "code mort" est surligné en gris. Le nombre à gauche du numéro de la ligne de code
indique combien de tests couvrent cette ligne.
Cliquer sur le numéro de ligne d'une ligne couverte ouvrira un panneau (voir Figure 14.2, « Panneau avec l'information des tests couvrant la ligne ») qui montre les cas de test qui couvrent cette ligne.
Le rapport de couverture de code de notre exemple CompteBancaire
montre que nous n'avons actuellement aucun test qui appellent les méthodes
setBalance(), deposerArgent() et
retirerArgent() avec des valeurs acceptables.
Exemple 14.1, « Test manquant pour atteindre la couverture de code complète »
montre un test qui peut être ajouté à la classe de cas de test
BankAccountTest pour couvrir complètement la classe
CompteBancaire.
Exemple 14.1. Test manquant pour atteindre la couverture de code complète
<?php
require_once 'CompteBancaire.php';
class BankAccountTest extends PHPUnit_Framework_TestCase
{
// ...
public function testDeposerRetirerArgent()
{
$this->assertEquals(0, $this->compte_bancaire->getBalance());
$this->compte_bancaire->deposerArgent(1);
$this->assertEquals(1, $this->compte_bancaire->getBalance());
$this->compte_bancaire->retirerArgent(1);
$this->assertEquals(0, $this->compte_bancaire->getBalance());
}
}
?>
Figure 14.3, « Couverture de code pour setBalance() avec un test additionnel » montre
la couverture de code de la méthode setBalance() avec le test
additionnel.
L'annotation @covers (voir
Tableau B.1, « Annotations pour indiquer quelles méthodes sont couvertes par un test ») peut être
utilisée dans le code de test pour indiquer quelle(s) méthode(s) une méthode de test
veut test. Si elle est fournie, seules les informations de couverture de code pour
la(les) méthode(s) indiquées seront prises en considération.
Exemple 14.2, « Tests qui indiquent quelle(s) méthode(s) ils veulent couvrir »
montre un exemple.
Exemple 14.2. Tests qui indiquent quelle(s) méthode(s) ils veulent couvrir
<?php
require_once 'CompteBancaire.php';
class CompteBancaireTest extends PHPUnit_Framework_TestCase
{
protected $compte_bancaire;
protected function setUp()
{
$this->compte_bancaire = new CompteBancaire;
}
/**
* @covers CompteBancaire::getBalance
*/
public function testBalanceEstInitialementZero()
{
$this->assertEquals(0, $this->compte_bancaire->getBalance());
}
/**
* @covers CompteBancaire::retirerArgent
*/
public function testBalanceNePeutPasDevenirNegative()
{
try {
$this->compte_bancaire->retirerArgent(1);
}
catch (CompteBancaireException $e) {
$this->assertEquals(0, $this->compte_bancaire->getBalance());
return;
}
$this->fail();
}
/**
* @covers CompteBancaire::deposerArgent
*/
public function testBalanceNePeutPasDevenirNegative2()
{
try {
$this->compte_bancaire->deposerArgent(-1);
}
catch (CompteBancaireException $e) {
$this->assertEquals(0, $this->compte_bancaire->getBalance());
return;
}
$this->fail();
}
/**
* @covers CompteBancaire::getBalance
* @covers CompteBancaire::deposerArgent
* @covers CompteBancaire::retirerArgent
*/
public function testDeposerArgent()
{
$this->assertEquals(0, $this->compte_bancaire->getBalance());
$this->compte_bancaire->deposerArgent(1);
$this->assertEquals(1, $this->compte_bancaire->getBalance());
$this->compte_bancaire->retirerArgent(1);
$this->assertEquals(0, $this->compte_bancaire->getBalance());
}
}
?>
Parfois, vous avez des blocs de code que vous ne pouvez pas tester et que
voulez ignorer lors de l'analyse de couverture de code. PHPUnit vous permet
de faire cela en utilisant les annotations
@codeCoverageIgnore,
@codeCoverageIgnoreStart et
@codeCoverageIgnoreEnd comme montré dans
Exemple 14.3, « Utiliser les annotations @codeCoverageIgnore, @codeCoverageIgnoreStart et @codeCoverageIgnoreEnd ».
Exemple 14.3. Utiliser les annotations @codeCoverageIgnore, @codeCoverageIgnoreStart et @codeCoverageIgnoreEnd
<?php
/**
* @codeCoverageIgnore
*/
class Foo
{
public function bar()
{
}
}
class Bar
{
/**
* @codeCoverageIgnore
*/
public function foo()
{
}
}
if (FALSE) {
// @codeCoverageIgnoreStart
print '*';
// @codeCoverageIgnoreEnd
}
?>
Les lignes de code qui sont marquées comme devant être ignorées en utilisant les annotations sont comptées comme exécutées (si elles sont exécutables) et ne seront pas surlignées.
Par défaut, tous les fichiers de code source qui contiennent au moins une ligne de code qui a été exécutée (et seulement ces fichiers) sont inclus dans le rapport. Les fichiers de code source qui sont inclus dans le rapport peuvent être filtrés en utilisant une approche par liste noire ou liste blanche.
La liste noire est pré-remplie avec tous les fichiers de code source de
PHPUnit lui-même ainsi que les tests. Quand la liste blanche est vide (par
défaut), le filtrage par liste noire est utilisé. Quand la liste blanche
n'est pas vide, le filtrage par liste blanche est utilisé. Quand le filtrage
par liste blanche est utilisé, chaque fichier de la liste blanche est optionnellement
ajouté (avec le réglage addUncoveredFilesFromWhiteList="true")
au rapport de couverture de code sans se soucier qu'il ait été exécuté ou pas.
Le fichier de configuration XML de PHPUnit (voir la section intitulée « Inclure et exclure des fichiers de la couverture de code ») peut être utilisé pour contrôler les listes noires et blanches. Utiliser une liste blanche est recommandé comme meilleure pratique pour contrôler la liste des fichiers inclus dans le rapport de couverture de code.
Dans la plupart des cas, on peut dire sans risque que PHPUnit offre une information de couverture de code "basée sur les lignes" mais du fait de la façon dont l'information est collectée, il existe quelques cas limites qui valent la peine d'être mentionnés.
Exemple 14.4.
<?php
// Parce qu'il s'agit d'une couverture "basée sur les lignes" et pas sur les instructions
// une ligne aura toujours un état de couverture donné
if(false) cet_appel_de_fonction_sera_compte_comme_couvert();
// Du fait de la façon dont la couverture de code fonctionne en interne, ces deux lignes sont spéciales.
/ Cette ligne sera comptée comme non exécutable
if(false)
// Cette ligne sera comptée comme couverte car c'est en fait la
// couverture de l'instruction if dans la ligne au-dessus qui
// sera montrée ici !
sera_egalement_comptee_comme_couverte();
// Pour éviter cela, il est nécessaire d'utiliser des accolades
if(false) {
cet_appel_ne_sera_jamais_compte_comme_couvert();
}
?>
Une fois que vous avez écrit des tests automatisés, vous découvrirez certainement davantage d'usages pour les tests. En voici quelques exemples.
Typiquement, dans un projet développé en utilisant un processus agile, tel que l'Extreme Programming, la documentation ne peut pas suivre les changements fréquents de la conception et du code du projet. l'Extreme Programming réclame la propriété collective du code, donc tous les développeurs ont besoin de savoir comment fonctionne l'intégralité du système. Si vous êtes suffisamment discipliné pour utiliser pour vos tests des "noms parlant" qui décrivent ce qu'une classe doit faire, vous pouvez utiliser la fonctionnalité TestDox de PHPUnit pour générer automatiquement de la documentation pour votre projet en s'appuyant sur ses tests. Cette documentation donne aux développeurs un aperçu de ce que chaque classe du projet est supposée faire.
La fonctionnalité TestDox de PHPUnit examine une classe de test et tous
les noms de méthode de test pour les convertir les noms au format Camel Case
PHP en phrases :
testBalanceEstInitialementAZéro() devient "Balance est
initialement a zero". S'il existe plusieurs méthodes de test dont les noms
ne diffèrent que par un suffixe constitué de un ou plusieurs chiffres, telles que
testBalanceNePeutPasEtreNégative() et
testBalanceNePeutPasEtreNégative2(), la phrase
"Balance ne peut pas etre négative" n'apparaîtra qu'une seule fois, en supposant que
tous ces tests ont réussi.
Jetons un oeil sur la documentation agile générée pour la classe
CompteBancaire (à partir de
Exemple 12.1, « Tests pour la classe CompteBancaire »):
phpunit --testdox CompteBancaireTest
PHPUnit 3.6.0 by Sebastian Bergmann.
CompteBancaire
[x] Balance est initialement a zéro
[x] Balance ne peut pas devenir négative
Alternativement, la documentation agile peut être générée en HTML ou au
format texte et écrite dans un fichier en utilisant les paramètres
--testdox-html et --testdox-text.
Note du traducteur: les majuscules accentuées ne sont pas correctement gérées, il ne faut donc pas les utiliser.
La documentation agile peut être utilisée pour documenter les hypothèses que vous faites sur les paquets externes que vous utilisez dans votre projet. Quand vous utilisez un paquet externe, vous vous exposez au risque que le paquet ne se comportera pas comme vous le prévoyez et que les futures versions du paquet changeront de façon subtile, ce qui cassera votre code sans que vous ne le sachiez. Vous pouvez adresser ces risques en écrivant un test à chaque fois que vous faites une hypothèse. Si votre test réussit, votre hypothèse est valide. Si vous documentez toutes vos hypothèses avec des tests, les futures livraisons du paquet externe ne poseront pas de problème : si les tests réussissent, votre système doit continuer à fonctionner.
Quand vous documentez des hypothèses avec des tests, vous êtes propriétaire des tests. Le fournisseur du paquet - sur lequel vous faîtes des hypothèses - ne connaît rien de vos tests. Si vous voulez avoir une relation plus étroite avec le fournisseur du paquet, vous pouvez utiliser les tests pour communiquer et coordonner vos activités.
Quand vous êtes d'accord pour coordonner vos activités avec le fournisseur d'un paquet, vous pouvez écrire les tests ensembles. Faites cela d'une telle façon que les tests révèlent autant d'hypothèses que possible. Les hypothèses cachées sont la mort de la coopération. Avec les tests, vous documentez exactement ce que vous attendez du paquet fourni. Le fournisseur saura que le paquet est prêt quand tous les tests fonctionneront.
En utilisant des bouchons (voir le chapitre relatif aux "objets simulacres", précédemment dans ce livre), vous pouvez créer un découplage plus grand entre vous et le fournisseur: le boulot du fournisseur est de faire que les tests fonctionnent avec l'implémentation réelle du paquet. Votre boulot est de faire que les tests fonctionnent sur votre propre code. Jusqu'à ce que vous ayez l'implémentation réelle du paquet fourni, vous utilisez des objets bouchons. Suivant cette approche, deux équipes peuvent développer indépendamment.
Le générateur de squelette de PHPUnit (Skeleton Generator) est un outil qui peut générer des squelettes de classes de test à partir des classes de code de production et vice versa. Il peut être installé en utilisant la commande suivante :
pear install phpunit/PHPUnit_SkeletonGeneratorQuand vous écrivez des tests pour du code existant, vous avez à écrire les mêmes fragments de code tels que
public function testMethode()
{
}encore et encore. Le générateur de squelette de PHPUnit peut vous aider en analysant le code d'une classe existante et en générant pour elle un squelette de classe de cas de test.
Exemple 16.1. La classe Calculateur
<?php
class Calculateur
{
public function additionner($a, $b)
{
return $a + $b;
}
}
?>
L'exemple suivant montre comment générer un squelette de classe de de test
pour une classe appelée Calculateur
(see Exemple 16.1, « La classe Calculateur »).
phpunit-skelgen --test Calculateur
PHPUnit Skeleton Generator 1.0.0 by Sebastian Bergmann.
Wrote skeleton for "CalculateurTest" to "/home/sb/CalculateurTest.php".Pour chaque méthode de la classe originelle, il y a aura un cas de test incomplet (voir Chapitre 9, Tests incomplets et sautés) dans la classe de cas de test générée.
Lorsque vous utilisez le générateur de squelette pour générer du code basé sur une classe qui est déclarée dans un espace de nommage (namespace) vous devez fournir le nom qualifié de la classe ainsi que le chemin d'accès au fichier source dans lequel elle est déclarée.
Par exemple, pour une classe Calculateur qui est déclarée
dans l'espace de nommage projet, vous devez invoquer le
générateur de squelette comme ceci :
phpunit-skelgen --test -- "projet\Calculateur" Calculateur.php
PHPUnit Skeleton Generator 1.0.0 by Sebastian Bergmann.
Wrote skeleton for "projet\CalculateurTest" to "/home/sb/CalculateurTest.php".
Ci-dessous se trouve la sortie écran produite par le lancement de la classe de cas de test générée.
phpunit --bootstrap Calculateur.php --verbose CalculateurTest
PHPUnit 3.6.8 by Sebastian Bergmann.
I
Time: 0 seconds, Memory: 3.50Mb
There was 1 incomplete test:
1) CalculateurTest::testAdditionner
This test has not been implemented yet.
/home/sb/CalculateurTest.php:38
OK, but incomplete or skipped tests!
Tests: 1, Assertions: 0, Incomplete: 1.
Vous pouvez utiliser l'annotation @assert dans le bloc
de documentation d'une méthode pour générer automatiquement des tests
simples mais significatifs au lieu de cas de tests incomplets.
Exemple 16.2, « La classe Calculateur avec des annotations @assert »
montre un exemple.
Exemple 16.2. La classe Calculateur avec des annotations @assert
<?php
class Calculateur
{
/**
* @assert (0, 0) == 0
* @assert (0, 1) == 1
* @assert (1, 0) == 1
* @assert (1, 1) == 2
*/
public function additionner($a, $b)
{
return $a + $b;
}
}
?>
Chaque méthode de la classe originelle est contrôlée à la recherche d'annotations
@assert. Celles-ci sont transformées en code de test comme
/**
* Generated from @assert (0, 0) == 0.
*/
public function testAdditionner() {
$o = new Calculateur;
$this->assertEquals(0, $o->additionner(0, 0));
}
Ci-dessous se trouve la sortie écran produite par le lancement de la classe de cas de test générée.
phpunit --bootstrap Calculateur.php --verbose CalculateurTest
PHPUnit 3.6.8 by Sebastian Bergmann.
....
Time: 0 seconds, Memory: 3.50Mb
OK (4 tests, 4 assertions)
Tableau 16.1, « Variantes gérées par l'annotation @assert »
montre les variantes gérées par l'annotation @assert
et de quelle façon elles sont transformées en code de test.
Tableau 16.1. Variantes gérées par l'annotation @assert
| Annotation | Transformée en |
|---|---|
@assert (...) == X | assertEquals(X, methode(...)) |
@assert (...) != X | assertNotEquals(X, methode(...)) |
@assert (...) === X | assertSame(X, methode(...)) |
@assert (...) !== X | assertNotSame(X, methode(...)) |
@assert (...) > X | assertGreaterThan(X, methode(...)) |
@assert (...) >= X | assertGreaterThanOrEqual(X, methode(...)) |
@assert (...) < X | assertLessThan(X, methode(...)) |
@assert (...) <= X | assertLessThanOrEqual(X, methode(...)) |
@assert (...) throws X | @expectedException X |
Lorsque vous pratiquez le développement dirigé par les tests (voir Chapitre 12, Développement dirigé par les tests) et que vous écrivez vos tests avant le code que les tests vérifient, PHPUnit peut vous aider à générer des squelettes de classe à partir des classes de cas de test.
Suivant la convention selon laquelle les tests pour une classe Unit
sont écrit dans une classe nommée UnitTest, le source de la classe
de cas de test est inspecté à la recherche de variables qui référencent des objets
de la classe Unit puis est analysé pour savoir quelles méthodes
sont appelées sur ces objets. Par exemple, jetez un oeil à Exemple 16.4, « Le squelette généré de la classe JeuDeBowling » qui a été généré en se
basant sur l'analyse de Exemple 16.3, « La classe JeuDeBowlingTest ».
Exemple 16.3. La classe JeuDeBowlingTest
<?php
class JeuDeBowlingTest extends PHPUnit_Framework_TestCase
{
protected $jeu;
protected function setUp()
{
$this->jeu = new JeuDeBowling;
}
protected function lancePlusieursEtRenverse($n, $quilles)
{
for ($i = 0; $i < $n; $i++) {
$this->jeu->renverse($quilles);
}
}
public function testScorePourJeuDansLaRigoleEst0()
{
$this->lancePlusieursEtRenverse(20, 0);
$this->assertEquals(0, $this->jeu->score());
}
}
?>
phpunit-skelgen --class JeuDeBowlingTest
PHPUnit Skeleton Generator 1.0.0 by Sebastian Bergmann.
Wrote skeleton for "JeuDeBowling" to "./JeuDeBowling.php".Exemple 16.4. Le squelette généré de la classe JeuDeBowling
<?php
/**
* Generated by PHPUnit_SkeletonGenerator on 2012-01-09 at 16:55:58.
*/
class JeuDeBowling
{
/**
* @todo Implement renverse().
*/
public function renverse()
{
// Remove the following line when you implement this method.
throw new RuntimeException('Not yet implemented.');
}
/**
* @todo Implement score().
*/
public function score()
{
// Remove the following line when you implement this method.
throw new RuntimeException('Not yet implemented.');
}
}
?>
Ci-dessous se trouve la sortie écran produite par le lancement de la classe de cas de test générée.
phpunit --bootstrap JeuDeBowling.php JeuDeBowlingTest
PHPUnit 3.6.8 by Sebastian Bergmann.
E
Time: 0 seconds, Memory: 3.50Mb
There was 1 error:
1) JeuDeBowlingTest::testScorePourJeuDansLaRigoleEst0
RuntimeException: Not yet implemented.
/home/sb/JeuDeBowling.php:13
/home/sb/JeuDeBowlingTest.php:14
/home/sb/JeuDeBowlingTest.php:20
FAILURES!
Tests: 1, Assertions: 0, Errors: 1.Selenium Server est un outil de test qui vous permet d'écrire des tests automatisés de l'interface utilisateur d'applications web dans n'importe quel langage et menés sur n'importe quel site web HTTP en utilisant n'importe quel navigateur courant. Il réalise des tâches automatisée dans le navigateur en pilotant le processus du navigateur via le système d'exploitation. Les tests Selenium s'exécutent directement dans un navigateur, exactement comme des utilisateurs réels le feraient. Ces tests peuvent être utilisés à la fois comme tests de validation (en exécutant des tests au plus haut niveau sur le système intégré au lieu de simplement tester chaque unité du système indépendamment) et des tests de compatibilité pour les navigateurs (en testant l'application web sur différents systèmes d'exploitation et différents navigateurs).
Le seul scénario géré par PHPUnit_Selenium est celui du serveur Selenium 2.x. Le serveur peut être accédé via l'API classique Selenium RC déjà présente dans la version 1.x ou avec l'API serveur WebDriver (partiellement implémentée) à partir de PHPUnit_Selenium 1.2.
La raison derrière cette décision est que Selenium 2 est rétro compatible et que Selenium RC n'est désormais plus maintenu.
Premièrement, installer Selenium Server:
selenium-server-standalone-2.20.0.jar (contrôler le suffixe de version) dans /usr/local/bin, par exemple.java -jar /usr/local/bin/selenium-server-standalone-2.20.0.jar.Deuxièmement, installer le paquet PHPUnit_Selenium, nécessaire pour accéder nativement au serveur Selenium depuis PHPUnit:
pear install phpunit/PHPUnit_Selenium
Maintenant, nous pouvons envoyer des commandes au serveur Selenium en utilisant son protocole client/serveur.
Le cas de test PHPUnit_Extensions_Selenium2TestCase vous permet d'utiliser l'API WebDriver (partiellement implémentée).
Exemple 17.1, « Exemple d'utilisation de PHPUnit_Extensions_Selenium2TestCase » montre
comment tester le contenu de l'élément <title>
du site web http://www.example.com/.
Exemple 17.1. Exemple d'utilisation de PHPUnit_Extensions_Selenium2TestCase
<?php
class WebTest extends PHPUnit_Extensions_Selenium2TestCase
{
protected function setUp()
{
$this->setBrowser('firefox');
$this->setBrowserUrl('http://www.example.com/');
}
public function testTitle()
{
$this->url('http://www.example.com/');
$this->assertEquals('Example WWW Page', $this->title());
}
}
?>
phpunit WebTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 28 seconds, Memory: 3.00Mb
There was 1 failure:
1) WebTest::testTitle
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'Example WWW Page'
+'IANA — Example domains'
/home/giorgio/WebTest.php:13
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.Les commandes de Selenium2TestCare sont implémentées via __call(). Merci de vous référer à the end-to-end test for PHPUnit_Extensions_Selenium2TestCase pour la liste de toutes les fonctionnalités prises en charge.
L'extension de cas de test PHPUnit_Extensions_SeleniumTestCase
implémente le protocole client/serveur pour parler au serveur Selenium ainsi que
des méthodes de vérification spécialisées pour le test web.
Exemple 17.2, « Exemple d'utilisation de PHPUnit_Extensions_SeleniumTestCase » montre
comment tester le contenu de l'élément <title>
du site web http://www.example.com/.
Exemple 17.2. Exemple d'utilisation de PHPUnit_Extensions_SeleniumTestCase
<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class WebTest extends PHPUnit_Extensions_SeleniumTestCase
{
protected function setUp()
{
$this->setBrowser('*firefox');
$this->setBrowserUrl('http://www.example.com/');
}
public function testTitle()
{
$this->open('http://www.example.com/');
$this->assertTitle('Example WWW Page');
}
}
?>
phpunit WebTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 9 seconds, Memory: 6.00Mb
There was 1 failure:
1) WebTest::testTitle
Current URL: http://www.iana.org/domains/example/
Failed asserting that 'IANA — Example domains' matches PCRE pattern "/Example WWW Page/".
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
Contrairement à la classe PHPUnit_Framework_TestCase,
les classes de cas de test qui héritent de PHPUnit_Extensions_SeleniumTestCase
doivent fournir une méthode setUp(). Cette méthode est utilisée
pour configurer la session du serveur Selenium. Voir
Tableau 17.1, « API de Selenium Server: configuration »
pour la liste des méthodes qui sont disponibles pour cela.
Tableau 17.1. API de Selenium Server: configuration
| Méthode | Signification |
|---|---|
void setBrowser(string $navigateur) | Règle le navigateur que le serveur Selenium Server doit utiliser. |
void setBrowserUrl(string $urlNavigateur) | Règle l'URL de base pour les tests. |
void setHost(string $hote) | Règle le nom d'hôte pour la connexion au serveur Selenium Server. |
void setPort(int $port) | Règle le port pour la connexion au serveur Selenium Server. |
void setTimeout(int $delaiExpiration) | Règle le délai d'expiration pour la connexion au serveur Selenium Server server. |
void setSleep(int $secondes) | Règle le nombre de secondes durant lesquelles le client Selenium Server client doit attendre entre l'envoi de commandes au serveur Selenium Server. |
PHPUnit peut facultativement faire une capture d'écran quand un test Selenium échoue. Pour
activer ceci, réglez $captureScreenshotOnFailure,
$screenshotPath et $screenshotUrl
dans votre classe de cas de test comme montré dans
Exemple 17.3, « Faire une capture d'écran quand un test échoue ».
Exemple 17.3. Faire une capture d'écran quand un test échoue
<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class WebTest extends PHPUnit_Extensions_SeleniumTestCase
{
protected $captureScreenshotOnFailure = TRUE;
protected $screenshotPath = '/var/www/localhost/htdocs/screenshots';
protected $screenshotUrl = 'http://localhost/screenshots';
protected function setUp()
{
$this->setBrowser('*firefox');
$this->setBrowserUrl('http://www.example.com/');
}
public function testTitle()
{
$this->open('http://www.example.com/');
$this->assertTitle('Example WWW Page');
}
}
?>
phpunit WebTest
PHPUnit 3.6.0 by Sebastian Bergmann.
F
Time: 7 seconds, Memory: 6.00Mb
There was 1 failure:
1) WebTest::testTitle
Current URL: http://www.iana.org/domains/example/
Screenshot: http://localhost/screenshots/334b080f2364b5f11568ee1c7f6742c9.png
Failed asserting that 'IANA — Example domains' matches PCRE pattern "/Example WWW Page/".
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
Vous pouvez exécuter chaque test en utilisant une série de navigateurs : au lieu
d'utiliser setBrowser() pour indiquer un seul navigateur, vous déclarez
un tableau public static nommé $browsers
dans votre classe de cas de test. Chaque élément de ce tableau décrit la configuration
d'un navigateur. Chacun de ces navigateurs peut être hébergé par différents serveurs
Selenium Server.
Exemple 17.4, « Régler la configuration de multiples navigateurs » montre
un exemple.
Exemple 17.4. Régler la configuration de multiples navigateurs
<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class WebTest extends PHPUnit_Extensions_SeleniumTestCase
{
public static $browsers = array(
array(
'name' => 'Firefox sur Linux',
'browser' => '*firefox',
'host' => 'ma.box.linux',
'port' => 4444,
'timeout' => 30000,
),
array(
'name' => 'Safari sur MacOS X',
'browser' => '*safari',
'host' => 'ma.box.macosx',
'port' => 4444,
'timeout' => 30000,
),
array(
'name' => 'Safari sur Windows XP',
'browser' => '*custom C:\Program Files\Safari\Safari.exe -url',
'host' => 'ma.box.windowsxp',
'port' => 4444,
'timeout' => 30000,
),
array(
'name' => 'Internet Explorer sur Windows XP',
'browser' => '*iexplore',
'host' => 'ma.box.windowsxp',
'port' => 4444,
'timeout' => 30000,
)
);
protected function setUp()
{
$this->setBrowserUrl('http://www.example.com/');
}
public function testTitle()
{
$this->open('http://www.example.com/');
$this->assertTitle('Example Web Page');
}
}
?>
PHPUnit_Extensions_SeleniumTestCase peut rassembler des informations
de couverture de code pour les tests lancés via Selenium:
PHPUnit/Extensions/SeleniumTestCase/phpunit_coverage.php dans le répertoire racine de votre serveur web.php.ini, configurez PHPUnit/Extensions/SeleniumTestCase/prepend.php et PHPUnit/Extensions/SeleniumTestCase/append.php respectivement comme auto_prepend_file et auto_append_file.PHPUnit_Extensions_SeleniumTestCase, utilisez protected $coverageScriptUrl = 'http://host/phpunit_coverage.php'; pour configurer l'URL pour le script phpunit_coverage.php.
Tableau 17.2, « Assertions » liste les diverses méthodes
de vérification que PHPUnit_Extensions_SeleniumTestCase
fournit.
Tableau 17.2. Assertions
| Assertion | Signification |
|---|---|
void assertElementValueEquals(string $localisateur, string $texte) | Rapporte une erreur si la valeur de l'élément identifié par $localisateur n'est pas égale au $texte donné. |
void assertElementValueNotEquals(string $localisateur, string $texte) | Rapporte une erreur si la valeur de l'élément identifié par $localisateur est égale au $texte donné. |
void assertElementValueContains(string $localisateur, string $texte) | Rapporte une erreur si la valeur de l'élément identifié par $localisateur ne contient pas le $texte donné. |
void assertElementValueNotContains(string $localisateur, string $texte) | Rapporte une erreur si la valeur de l'élément identifié par $localisateur contient le $texte donné. |
void assertElementContainsText(string $localisateur, string $texte) | Rapporte une erreur si l'élément identifié par $localisateur ne contient pas le $texte donné. |
void assertElementNotContainsText(string $localisateur, string $texte) | Rapporte une erreur si l'élément identifié par $localisateur contient le $texte donné. |
void assertSelectHasOption(string $localisateurDeSelect, string $option) | Rapporte une erreur si l'option de liste déroulante donnée n'est pas disponible. |
void assertSelectNotHasOption(string $localisateurDeSelect, string $option) | Rapporte une erreur si l'option de liste déroulante donnée est disponible. |
void assertSelected($localisateurDeSelect, $option) | Rapporte une erreur si l'étiquette de liste déroulante donnée n'est pas sélectionnée. |
void assertNotSelected($localisateurDeSelect, $option) | Rapporte une erreur si l'étiquette de liste déroulante donnée est sélectionnée. |
void assertIsSelected(string $localisateurDeSelect, string $valeur) | Rapporte une erreur si la valeur donnée n'est pas sélectionnée dans la liste déroulante. |
void assertIsNotSelected(string $localisateurDeSelect, string $valeur) | Rapporte une erreur si la valeur donnée est sélectionnée dans la liste déroulante. |
Tableau 17.3, « Méthodes canevas » montre
la méthode canevas de PHPUnit_Extensions_SeleniumTestCase:
Tableau 17.3. Méthodes canevas
| Méthode | Signification |
|---|---|
void defaultAssertions() | Surcharge pour exécuter des assertions qui sont partagées par tous les tests d'un cas de test. Cette méthode est appelée après chaque commande qui est envoyée au serveur Selenium Server. |
Merci de vous référer à la documentation des commandes Selenium pour une référence des commandes disponibles et comment elles sont utilisées.
Les commandes de Selenium 1 sont implémentées dynamiquement via __call. Référez-vous également aux documents de l'API pour PHPUnit_Extensions_SeleniumTestCase_Driver::__call() pour une liste de toutes les méthodes gérées du côté PHP, avec les paramètres et le type de retourné quand ils sont disponibles.
En utilisant la méthode runSelenese($filename), vous pouvez également
lancer un test Selenium à partir de ses spécifications Selenese/HTML. Plus encore,
en utilisant l'attribut statique $seleneseDirectory, vous pouvez
créer automatiquement des objets tests à partir d'un répertoire qui contient
des fichiers Selenese/HTML. Le répertoire indiqué est parcouru récursivement
à la recherche de fichiers .htm qui sont supposés contenir du Selenese/HTML.
Exemple 17.5, « Utiliser un répertoire de fichiers Selenese/HTML comme tests » montre un
exemple.
Exemple 17.5. Utiliser un répertoire de fichiers Selenese/HTML comme tests
<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class SeleneseTests extends PHPUnit_Extensions_SeleniumTestCase
{
public static $seleneseDirectory = '/chemin/vers/fichiers';
}
?>
A partir de Selenium 1.1.1, une fonctionnalité expérimentale est incluse permettant à un utilisateur de partager la session entre plusieurs tests. Le seul cas géré est le partage de session entre tous les tests quand un unique navigateur est utilisé.
Appelez PHPUnit_Extensions_SeleniumTestCase::shareSession(true) dans votre fichier amorce pour activer le partage de session.
La session sera réinitialisée dans le cas où un test échoue (en échec ou incomplet); c'est à la charge de l'utilisateur d'éviter les interactions entre des tests en réinitialisant des cookies ou en se déconnectant de l'application testée (avec une méthode tearDown()).
PHPUnit peut produire plusieurs types de fichiers de journalisations (logs).
Le fichier de journalisation XML pour les tests produits par PHPUnit est basé sur celui
qui est utilisé par la tâche
JUnit
de l'outil Apache Ant. L'exemple suivant montre que le fichier
de journalisation XML généré pour les tests dans
TestTableau:
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="TestTableau"
file="/home/sb/TestTableau.php"
tests="2"
assertions="2"
failures="0"
errors="0"
time="0.016030">
<testcase name="testLeNouveauTableauEstVide"
class="TestTableau"
file="/home/sb/TestTableau.php"
line="6"
assertions="1"
time="0.008044"/>
<testcase name="testLeTableauContientUnElement"
class="TestTableau"
file="/home/sb/TestTableau.php"
line="15"
assertions="1"
time="0.007986"/>
</testsuite>
</testsuites>
Le fichier de journalisation XML suivant a été généré pour deux tests,
testEchec et testErreur,
à partir d'une classe de cas de test nommée EchecErreurTest et
montre comment les échecs et les erreurs sont signalés.
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="EchecErreurTest"
file="/home/sb/EchecErreurTest.php"
tests="2"
assertions="1"
failures="1"
errors="1"
time="0.019744">
<testcase name="testFailure"
class="EchecErreurTest"
file="/home/sb/EchecErreurTest.php"
line="6"
assertions="1"
time="0.011456">
<failure type="PHPUnit_Framework_ExpectationFailedException">
testFailure(EchecErreurTest)
Failed asserting that <integer:2> matches expected value <integer:1>.
/home/sb/EchecErreurTest.php:8
</failure>
</testcase>
<testcase name="testError"
class="EchecErreurTest"
file="/home/sb/EchecErreurTest.php"
line="11"
assertions="0"
time="0.008288">
<error type="Exception">testError(EchecErreurTest)
Exception:
/home/sb/EchecErreurTest.php:13
</error>
</testcase>
</testsuite>
</testsuites>
Le protocole Test Anything Protocol (TAP)
est une interface Perl simple au format texte entre les modules de test. L'exemple
suivant montre le fichier de journalisation TAP généré pour les tests de
TableauTest:
TAP version 13 ok 1 - testNouveauTableauEstVide(TableauTest) ok 2 - testTableauContientUnElement(TableauTest) 1..2
Le fichier de journalisation TAP a été généré pour deux tests,
testEchec et testErreur
à partir d'une classe de cas de test nommée EchecErreurTest et
montre comme les échecs et les erreurs sont signalés.
TAP version 13
not ok 1 - Failure: testEchec(EchecErreurTest)
---
message: 'Failed asserting that <integer:2> matches expected value <integer:1>.'
severity: fail
data:
got: 2
expected: 1
...
not ok 2 - Error: testErreur(EchecErreurTest)
1..2
La notation objet JavaScript (JavaScript Object Notation ou JSON)
est un format léger d'échange de données. L'exemple suivant montre les messages JSON
générés pour les tests dans TableauTest:
{"event":"suiteStart","suite":"TableauTest","tests":2}
{"event":"test","suite":"TableauTest",
"test":"testNouveauTableauEstVide(TableauTest)","status":"pass",
"time":0.000460147858,"trace":[],"message":""}
{"event":"test","suite":"TableauTest",
"test":"testTableauContientUnElement(TableauTest)","status":"pass",
"time":0.000422954559,"trace":[],"message":""}
Les messages JSON suivants ont été générés pour deux tests,
testEchec et testErreur,
d'une classe de cas de test nommée EchecErreurTest et
monte comment les échecs et les erreurs sont signalées.
{"event":"suiteStart","suite":"EchecErreurTest","tests":2}
{"event":"test","suite":"EchecErreurTest",
"test":"testEchec(EchecErreurTest)","status":"fail",
"time":0.0082459449768066,"trace":[],
"message":"Failed asserting that <integer:2> is equal to <integer:1>."}
{"event":"test","suite":"EchecErreurTest",
"test":"testErreur(EchecErreurTest)","status":"error",
"time":0.0083680152893066,"trace":[],"message":""}
La journalisation au format XML des informations de couverture de code produite par PHPUnit
est faiblement basé sur celui utilisé par
Clover. L'exemple suivant montre le fichier de journalisation XML
généré pour les tests dans CompteBancaireTest:
<?xml version="1.0" encoding="UTF-8"?>
<coverage generated="1184835473" phpunit="3.6.0">
<project name="CompteBancaireTest" timestamp="1184835473">
<file name="/home/sb/CompteBancaire.php">
<class name="CompteBancaireException">
<metrics methods="0" coveredmethods="0" statements="0"
coveredstatements="0" elements="0" coveredelements="0"/>
</class>
<class name="CompteBancaire">
<metrics methods="4" coveredmethods="4" statements="13"
coveredstatements="5" elements="17" coveredelements="9"/>
</class>
<line num="77" type="method" count="3"/>
<line num="79" type="stmt" count="3"/>
<line num="89" type="method" count="2"/>
<line num="91" type="stmt" count="2"/>
<line num="92" type="stmt" count="0"/>
<line num="93" type="stmt" count="0"/>
<line num="94" type="stmt" count="2"/>
<line num="96" type="stmt" count="0"/>
<line num="105" type="method" count="1"/>
<line num="107" type="stmt" count="1"/>
<line num="109" type="stmt" count="0"/>
<line num="119" type="method" count="1"/>
<line num="121" type="stmt" count="1"/>
<line num="123" type="stmt" count="0"/>
<metrics loc="126" ncloc="37" classes="2" methods="4" coveredmethods="4"
statements="13" coveredstatements="5" elements="17"
coveredelements="9"/>
</file>
<metrics files="1" loc="126" ncloc="37" classes="2" methods="4"
coveredmethods="4" statements="13" coveredstatements="5"
elements="17" coveredelements="9"/>
</project>
</coverage>
Sortie de couverture de code humainement lisible pour la ligne de commandes ou un fichier texte.
Le but de ce format de sortie est de fournir un aperçu rapide de couverture
en travaillant sur un petit ensemble de classes. Pour des projets plus grand
cette sortie peut être utile pour obtenir un aperçu rapide de la couverture
des projets ou quand il est utilisé avec la fonctionnalité
--filter.
Quand c'est utilisé à partir de la ligne de commande en écrivant sur
php://stdout, cela prend en compte le réglage
--colors.
Ecrire sur la sortie standard est l'option par défaut quand on utilise la ligne
de commandes.
Par défaut, ceci ne montrera que les fichiers qui ont au moins une ligne couverte.
Ceci peut être modifié via l'option de configuration xml
showUncoveredFiles
Voir la section intitulée « Journalisation ».
PHPUnit peut être étendu de multiples façon pour rendre l'écriture des tests plus facile et personnaliser le retour que vous obtenez des tests exécutés. Voici les points de départs communs pour étendre PHPUnit.
Ecrivez des assertions personnalisées et des méthodes utilitaires dans une
sous classe abstraite de
PHPUnit_Framework_TestCase et faites hériter vos classes
de cas de test de cette classe. C'est une des façon les plus faciles pour
étendre PHPUnit.
Lorsqu'on écrit des assertions personnalisées, une bonne pratique
consiste à suivre la façon dont PHPUnit implémente ses propres assertions.
Comme vous pouvez le voir dans
Exemple 19.1, « Les méthodes assertTrue() et isTrue() de la classe PHPUnit_Framework_Assert », la méthode
assertTrue() n'est qu'un enrobeur des méthodes
isTrue() et assertThat():
isTrue() crée un objet de correspondance qui est passé à
assertThat() pour évaluation.
Exemple 19.1. Les méthodes assertTrue() et isTrue() de la classe PHPUnit_Framework_Assert
<?php
abstract class PHPUnit_Framework_Assert
{
// ...
/**
* Asserts that a condition is true.
*
* @param boolean $condition
* @param string $message
* @throws PHPUnit_Framework_AssertionFailedError
*/
public static function assertTrue($condition, $message = '')
{
self::assertThat($condition, self::isTrue(), $message);
}
// ...
/**
* Returns a PHPUnit_Framework_Constraint_IsTrue matcher object.
*
* @return PHPUnit_Framework_Constraint_IsTrue
* @since Method available since Release 3.3.0
*/
public static function isTrue()
{
return new PHPUnit_Framework_Constraint_IsTrue;
}
// ...
}?>
Exemple 19.2, « La classe PHPUnit_Framework_Constraint_IsTrue » montre comment
PHPUnit_Framework_Constraint_IsTrue étend la classe
abstraite de base pour des objets de correspondance (ou des contraintes),
PHPUnit_Framework_Constraint.
Exemple 19.2. La classe PHPUnit_Framework_Constraint_IsTrue
<?php
class PHPUnit_Framework_Constraint_IsTrue extends PHPUnit_Framework_Constraint
{
/**
* Evaluates the constraint for parameter $other. Returns TRUE if the
* constraint is met, FALSE otherwise.
*
* @param mixed $other Value or object to evaluate.
* @return bool
*/
public function evaluate($other)
{
return $other === TRUE;
}
/**
* Returns a string representation of the constraint.
*
* @return string
*/
public function toString()
{
return 'is true';
}
}?>
L'effort d'implémentation des méthodes assertTrue() et
isTrue() ainsi que la classe
PHPUnit_Framework_Constraint_IsTrue tire bénéfice du fait que
assertThat() prend automatiquement soin d'évaluer l'assertion et
les tâches de suivi comme le décompte à des fins de statistique.
Plus encore, la méthode isTrue() peut être utilisée comme un matcher
lors de la configuration d'objets simulacres.
Exemple 19.3, « Un simple moniteur de test »
montre une implémentation simple de l'interface PHPUnit_Framework_TestListener.
Exemple 19.3. Un simple moniteur de test
<?php
class SimpleTestListener implements PHPUnit_Framework_TestListener
{
public function addError(PHPUnit_Framework_Test $test, Exception $e, $time)
{
printf("Error while running test '%s'.\n", $test->getName());
}
public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time)
{
printf("Test '%s' failed.\n", $test->getName());
}
public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time)
{
printf("Test '%s' is incomplete.\n", $test->getName());
}
public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time)
{
printf("Test '%s' has been skipped.\n", $test->getName());
}
public function startTest(PHPUnit_Framework_Test $test)
{
printf("Test '%s' started.\n", $test->getName());
}
public function endTest(PHPUnit_Framework_Test $test, $time)
{
printf("Test '%s' ended.\n", $test->getName());
}
public function startTestSuite(PHPUnit_Framework_TestSuite $suite)
{
printf("TestSuite '%s' started.\n", $suite->getName());
}
public function endTestSuite(PHPUnit_Framework_TestSuite $suite)
{
printf("TestSuite '%s' ended.\n", $suite->getName());
}
}
?>
Dans la section intitulée « Moniteurs de tests » vous pouvez voir comment configurer PHPUnit pour brancher votre moniteur de test lors de l'exécution des tests.
Vous pouvez encapsuler des cas de test ou des séries de tests dans une
sous-classe de PHPUnit_Extensions_TestDecorator et utiliser
le Design Pattern Decorator pour réaliser certaines actions avant et après
que les tests sont exécutés.
PHPUnit apporte deux décorateurs de test concrets:
PHPUnit_Extensions_RepeatedTest et
PHPUnit_Extensions_TestSetup. Le premier est utilisé pour
exécuter de manière répétée un test et ne le comptabiliser comme succès que si
toutes les itérations ont réussi. Le second est discuté dans Chapitre 6, Fixtures.
Exemple 19.4, « Le décorateur RepeatedTest »
montre une version raccourcie du décorateur de test
PHPUnit_Extensions_RepeatedTest qui illustre comment
écrire vos propres décorateurs de tests.
Exemple 19.4. Le décorateur RepeatedTest
<?php
require_once 'PHPUnit/Extensions/TestDecorator.php';
class PHPUnit_Extensions_RepeatedTest extends PHPUnit_Extensions_TestDecorator
{
private $timesRepeat = 1;
public function __construct(PHPUnit_Framework_Test $test, $timesRepeat = 1)
{
parent::__construct($test);
if (is_integer($timesRepeat) &&
$timesRepeat >= 0) {
$this->timesRepeat = $timesRepeat;
}
}
public function count()
{
return $this->timesRepeat * $this->test->count();
}
public function run(PHPUnit_Framework_TestResult $result = NULL)
{
if ($result === NULL) {
$result = $this->createResult();
}
for ($i = 0; $i < $this->timesRepeat && !$result->shouldStop(); $i++) {
$this->test->run($result);
}
return $result;
}
}
?>
L'interface PHPUnit_Framework_Test est restreinte et
facile à implémenter. Vous pouvez écrire une implémentation de
PHPUnit_Framework_Test qui est plus simple que
PHPUnit_Framework_TestCase et qui exécute
des tests dirigés par les données, par exemple.
Exemple 19.5, « Un test dirigé par les données »
montre une classe de cas de test dirigé par les tests qui compare les
valeurs d'un fichier contenant des valeurs séparées par des virgules (CSV).
Chaque ligne d'un tel fichier ressemble à
foo;bar, où la première valeur est celle que nous attendons
et la seconde valeur celle constatée.
Exemple 19.5. Un test dirigé par les données
<?php
class DirigeParLesDonneesTest implements PHPUnit_Framework_Test
{
private $lines;
public function __construct($dataFile)
{
$this->lines = file($dataFile);
}
public function count()
{
return 1;
}
public function run(PHPUnit_Framework_TestResult $result = NULL)
{
if ($result === NULL) {
$result = new PHPUnit_Framework_TestResult;
}
foreach ($this->lines as $line) {
$result->startTest($this);
PHP_Timer::start();
$stopTime = NULL;
list($expected, $actual) = explode(';', $line);
try {
PHPUnit_Framework_Assert::assertEquals(
trim($expected), trim($actual)
);
}
catch (PHPUnit_Framework_AssertionFailedError $e) {
$stopTime = PHP_Timer::stop();
$result->addFailure($this, $e, $stopTime);
}
catch (Exception $e) {
$stopTime = PHP_Timer::stop();
$result->addError($this, $e, $stopTime);
}
if ($stopTime === NULL) {
$stopTime = PHP_Timer::stop();
}
$result->endTest($this, $stopTime);
}
return $result;
}
}
$test = new DataDrivenTest('fichier_donnees.csv');
$resultat = PHPUnit_TextUI_TestRunner::run($test);
?>
PHPUnit 3.6.0 by Sebastian Bergmann. .F Time: 0 seconds There was 1 failure: 1) DirigeParLesDonneesTest Failed asserting that two strings are equal. expected string <bar> difference < x> got string <baz> /home/sb/DirigeParLesDonneesTest.php:32 /home/sb/DirigeParLesDonneesTest.php:53 FAILURES! Tests: 2, Failures: 1.
Tableau A.1, « Assertions » montre toutes les variétés d'assertions.
Tableau A.1. Assertions
| Assertion |
|---|
assertArrayHasKey($clef, array $tableau, $message = '') |
assertArrayNotHasKey($clef, array $tableau, $message = '') |
assertAttributeContains($aiguille, $nomAttributMeuleDeFoin, $classeOuObjetMeuleDeFoin, $message = '', $ignoreCasse = FALSE, $controleIdentiteDesObjets = TRUE) |
assertAttributeContainsOnly($type, $nomAttributMeuleDeFoin, $classeOuObjetMeuleDeFoin, $estTypeNatif = NULL, $message = '') |
assertAttributeEmpty($nomAttributMeuleDeFoin, $classeOuObjetMeuleDeFoin, $message = '') |
assertAttributeEquals($attendu, $nomAttributConstate, $classeOuObjetConstate, $message = '', $delta = 0, $profondeurMax = 10, $canoniser = FALSE, $ignoreCasse = FALSE) |
assertAttributeGreaterThan($attendu, $nomAttributConstate, $classeOuObjetConstate, $message = '') |
assertAttributeGreaterThanOrEqual($attendu, $nomAttributConstate, $classeOuObjetConstate, $message = '') |
assertAttributeInstanceOf($attendu, $nomAttribut, $classeOuObjet, $message = '') |
assertAttributeInternalType($attendu, $nomAttribut, $classeOuObjet, $message = '') |
assertAttributeLessThan($attendu, $nomAttributConstate, $classeOuObjetConstate, $message = '') |
assertAttributeLessThanOrEqual($attendu, $nomAttributConstate, $classeOuObjetConstate, $message = '') |
assertAttributeNotContains($aiguille, $nomAttributMeuleDeFoin, $classeOuObjetMeuleDeFoin, $message = '', $ignoreCasse = FALSE, $verifierIdenditeDesObjets = TRUE) |
assertAttributeNotContainsOnly($type, $nomAttributMeuleDeFoin, $classeOuObjetMeuleDeFoin, $estTypeNatif = NULL, $message = '') |
assertAttributeNotEmpty($nomAttributMeuleDeFoin, $classeOuObjetMeuleDeFoin, $message = '') |
assertAttributeNotEquals($attendu, $nomAttributConstate, $classeOuObjetConstate, $message = '', $delta = 0, $profondeurMax = 10, $canoniser = FALSE, $ignoreCasse = FALSE) |
assertAttributeNotInstanceOf($attendu, $nomAttribut, $classeOuObjet, $message = '') |
assertAttributeNotInternalType($attendu, $nomAttribut, $classeOuObjet, $message = '') |
assertAttributeNotSame($attendu, $nomAttributConstate, $classeOuObjetConstate, $message = '') |
assertAttributeSame($attendu, $nomAttributConstate, $classeOuObjetConstate, $message = '') |
assertClassHasAttribute($nomAttribut, $nomClasse, $message = '') |
assertClassHasStaticAttribute($nomAttribut, $nomClasse, $message = '') |
assertClassNotHasAttribute($nomAttribut, $nomClasse, $message = '') |
assertClassNotHasStaticAttribute($nomAttribut, $nomClasse, $message = '') |
assertContains($aiguille, $meuleDeFoin, $message = '', $ignoreCasse = FALSE, $verifierIdenditeDesObjets = TRUE) |
assertContainsOnly($type, $meuleDeFoin, $estTypeNatif = NULL, $message = '') |
assertCount($nombreAttendu, $meuleDeFoin, $message = '') |
assertEmpty($constate, $message = '') |
assertEqualXMLStructure(DOMElement $elementAttendu, DOMElement $elementConstate, $controlerAttributs = FALSE, $message = '') |
assertEquals($attendu, $constate, $message = '', $delta = 0, $profondeurMax = 10, $canoniser = FALSE, $ignoreCasse = FALSE) |
assertFalse($condition, $message = '') |
assertFileEquals($attendu, $constate, $message = '', $canoniser = FALSE, $ignoreCasse = FALSE) |
assertFileExists($nomFichier, $message = '') |
assertFileNotEquals($attendu, $constate, $message = '', $canoniser = FALSE, $ignoreCasse = FALSE) |
assertFileNotExists($nomFichier, $message = '') |
assertGreaterThan($attendu, $constate, $message = '') |
assertGreaterThanOrEqual($attendu, $constate, $message = '') |
assertInstanceOf($attendu, $constate, $message = '') |
assertInternalType($attendu, $constate, $message = '') |
assertLessThan($attendu, $constate, $message = '') |
assertLessThanOrEqual($attendu, $constate, $message = '') |
assertNotContains($aiguille, $meuleDeFoin, $message = '', $ignoreCasse = FALSE, $verifierIdenditeDesObjets = TRUE) |
assertNotContainsOnly($type, $meuleDeFoin, $estTypeNatif = NULL, $message = '') |
assertNotCount($nombreAttendu, $meuleDeFoin, $message = '') |
assertNotEmpty($constate, $message = '') |
assertNotEquals($attendu, $constate, $message = '', $delta = 0, $profondeurMax = 10, $canoniser = FALSE, $ignoreCasse = FALSE) |
assertNotInstanceOf($attendu, $constate, $message = '') |
assertNotInternalType($attendu, $constate, $message = '') |
assertNotNull($constate, $message = '') |
assertNotRegExp($motif, $chaine, $message = '') |
assertNotSame($attendu, $constate, $message = '') |
assertNotTag($matcher, $constate, $message = '', $estHtml = TRUE) |
assertNull($constate, $message = '') |
assertObjectHasAttribute($nomAttribut, $object, $message = '') |
assertObjectNotHasAttribute($nomAttribut, $object, $message = '') |
assertRegExp($motif, $chaine, $message = '') |
assertSame($attendu, $constate, $message = '') |
assertSelectCount($selecteur, $nombre, $constate, $message = '', $estHtml = TRUE) |
assertSelectEquals($selecteur, $contenu, $nombre, $constate, $message = '', $estHtml = TRUE) |
assertSelectRegExp($selecteur, $motif, $nombre, $constate, $message = '', $estHtml = TRUE) |
assertStringEndsNotWith($suffixe, $chaine, $message = '') |
assertStringEndsWith($suffixe, $chaine, $message = '') |
assertStringEqualsFile($fichierAttendu, $chaineConstatee, $message = '', $canoniser = FALSE, $ignoreCasse = FALSE) |
assertStringMatchesFormat($format, $chaine, $message = '') |
assertStringMatchesFormatFile($fichierFormat, $chaine, $message = '') |
assertStringNotEqualsFile($fichierAttendu, $chaineConstatee, $message = '', $canoniser = FALSE, $ignoreCasse = FALSE) |
assertStringNotMatchesFormat($format, $chaine, $message = '') |
assertStringNotMatchesFormatFile($fichierFormat, $chaine, $message = '') |
assertStringStartsNotWith($prefixe, $chaine, $message = '') |
assertStringStartsWith($prefixe, $chaine, $message = '') |
assertTag($matcher, $constate, $message = '', $estHtml = TRUE) |
assertThat($value, PHPUnit_Framework_Constraint $contrainte, $message = '') |
assertTrue($condition, $message = '') |
assertXmlFileEqualsXmlFile($fichierAttendu, $fichierConstate, $message = '') |
assertXmlFileNotEqualsXmlFile($fichierAttendu, $fichierConstate, $message = '') |
assertXmlStringEqualsXmlFile($fichierAttendu, $xmlConstate, $message = '') |
assertXmlStringEqualsXmlString($xmlAttendu, $xmlConstate, $message = '') |
assertXmlStringNotEqualsXmlFile($fichierAttendu, $xmlConstate, $message = '') |
assertXmlStringNotEqualsXmlString($xmlAttendu, $xmlConstate, $message = '') |
Une annotation est une forme spéciale de méta donnée syntaxique qui peut
être ajoutée au code source de certains langages de programmation. Bien que
PHP n'ait pas de fonctionnalité dédiée à l'annotation du code source, l'utilisation
d'étiquettes telles que @annotation paramètres dans les blocs de documentation
s'est établi dans la communauté PHP pour annoter le code source. En PHP, les blocs de
documentation sont réflexifs: ils peuvent être accédés via la méthode de l'API de réflexivité
getDocComment() au niveau des fonctions, classes, méthodes et attributs.
Des applications telles que PHPUnit utilisent ces informations durant l'exécution pour
adapter leur comportement.
Cette annexe montre toutes les sortes d'annotations gérées par PHPUnit.
Vous pouvez utiliser l'annotation @assert dans le bloc
de documentation d'une méthode pour générer automatiquement des tests
simples mais significatifs au lieu des cas de test incomplets quand on utilise
le générateur de squelette (voir Chapitre 16, Générateur de squelette):
/**
* @assert (0, 0) == 0
*/
public function additionne($a, $b)
{
return $a + $b;
}Ces annotations sont transformés en code de test comme
/**
* Generated from @assert (0, 0) == 0.
*/
public function testAdditionne() {
$o = new Calculateur;
$this->assertEquals(0, $o->additionne(0, 0));
}
L'annotation @author est un alias pour l'annotation
@group (voir la section intitulée « @group ») et permet de filtrer des tests selon
leurs auteurs.
Les opérations de sauvegarde et de restauration des variables globales peuvent être complètement désactivées pour tous les tests d'une classe de cas de test comme ceci :
/**
* @backupGlobals disabled
*/
class MonTest extends PHPUnit_Framework_TestCase
{
// ...
}
L'annotation @backupGlobals peut également être utilisée sur les opérations
de sauvegarde et de restauration :
/**
* @backupGlobals disabled
*/
class MonTest extends PHPUnit_Framework_TestCase
{
/**
* @backupGlobals enabled
*/
public function testQuiIntergitAvecDesVariablesGlobales()
{
// ...
}
}
Les opérations de sauvegarde et de restauration pour les attributs statiques des classes peuvent être complètement désactivés pour tous les tests d'une classe de cas de test comme ceci :
/**
* @backupStaticAttributes disabled
*/
class MonTest extends PHPUnit_Framework_TestCase
{
// ...
}
L'annotation @backupStaticAttributes peut également être utilisée au
niveau d'une méthode de test. Ceci permet une configuration plus fine des opérations
de sauvegarde et de restauration:
/**
* @backupStaticAttributes disabled
*/
class MonTest extends PHPUnit_Framework_TestCase
{
/**
* @backupStaticAttributes enabled
*/
public function testQuiInteragitAvecDesAttributsStatiques()
{
// ...
}
}
Les annotations @codeCoverageIgnore,
@codeCoverageIgnoreStart et
@codeCoverageIgnoreEnd peuvent être utilisées pour
exclure des lignes de code de l'analyse de couverture.
Pour la manière de les utiliser, voir la section intitulée « Ignorer des blocs de code ».
L'annotation @covers peut être utilisée dans le code de test pour
indique quelle(s) méthode(s) un test veut tester:
/**
* @covers CompteBancaire::getBalance
*/
public function testBalanceEstInitiallementAZero()
{
$this->assertEquals(0, $this->ba->getBalance());
}
Si elle est fournie, seule l'information de couverture de code pour la(les) méthode(s) sera prise en considération.
Tableau B.1, « Annotations pour indiquer quelles méthodes sont couvertes par un test » montre
la syntaxe de l'annotation @covers.
Tableau B.1. Annotations pour indiquer quelles méthodes sont couvertes par un test
| Annotation | Description |
|---|---|
@covers NomClasse::nomMethode | Indique que la méthode de test annotée couvre la méthode indiquée. |
@covers NomClasse | Indique que la méthode de test annotée couvre toutes les méthodes d'une classe donnée. |
@covers NomClasse<extended> | Indique que la méthode de test annotée couvre toutes les méthodes d'une classe donnée ainsi que les classe(s) et interface(s) parentes. |
@covers NomClasse::<public> | Indique que la méthode de test annotée couvre toutes les méthodes publiques d'une classe donnée. |
@covers NomClasse::<protected> | Indique que la méthode de test annotée couvre toutes les méthodes protected d'une classe donnée. |
@covers NomClasse::<private> | Indique que la méthode de test annotée couvre toutes les méthodes privées d'une classe donnée. |
@covers NomClasse::<!public> | Indique que la méthode de test annotée couvre toutes les méthodes d'une classe donnée qui ne sont pas publiques. |
@covers NomClasse::<!protected> | Indique que la méthode de test annotée couvre toutes les méthodes d'une classe donnée qui ne sont pas protected. |
@covers NomClasse::<!private> | Indique que la méthode de test annotée couvre toutes les méthodes d'une classe donnée qui ne sont pas privées. |
Une méthode de test peut accepter des paramètres arbitraires. Ces paramètres
peuvent être fournis pas une méthode fournisseuse de données (
(provider() dans
Exemple 4.4, « Utiliser un fournisseur de données qui renvoie un tableau de tableaux »).
La méthode fournisseur de données peut être indiquée en utilisant l'annotation
@dataProvider.
Voir la section intitulée « Fournisseur de données » pour plus de détails.
PHPUnit gère la déclaration des dépendances explicites entre les méthodes
de test. De telles dépendances ne définissent pas l'ordre dans lequel les
méthodes de test doivent être exécutées mais elles permettent de retourner
l'instance d'une fixture de test par un producteur et de la passer aux
consommateurs dépendants.
Exemple 4.2, « Utiliser l'annotation @depends pour exprimer des dépendances » montre
comment utiliser l'annotation @depends pour exprimer des
dépendances entre méthodes de test.
Voir la section intitulée « Dépendances des tests » pour plus de détails.
Exemple 4.7, « Utiliser l'annotation @expectedException »
montre comment utiliser l'annotation @expectedException pour tester
si une exception est levée dans le code testé.
Voir la section intitulée « Tester des exceptions » pour plus de détails.
L'annotation @expectedExceptionCode, en conjonction avec
@expectedException permet de faire des assertions sur le
code d'erreur d'une exception levée ce qui permet de cibler une exception
particulière.
class MonTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException MonException
* @expectedExceptionCode 20
*/
public function testExceptionAUnCodeErreur20()
{
throw new MonException('Un message', 20);
}
}
L'annotation @expectedExceptionMessage fonctionne de manière
similaire à @expectedExceptionCode en ce qu'il vous permet de
faire une assertion sur le message d'erreur d'une exception.
class MonTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException MonException
* @expectedExceptionMessage Un message
*/
public function testExceptionALeBonMessage()
{
throw new MonException('Un message', 20);
}
}Le message attendu peut être une partie d'une chaîne d'un message d'exception. Ceci peut être utile pour faire une assertion sur le fait qu'un nom ou un paramètre qui est passé s'affiche dans une exception sans fixer la totalité du message d'exception dans le test.
class MonTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException MonException
* @expectedExceptionMessage cassé
*/
public function testExceptionALeBonMessage()
{
$param = "cassé";
throw new MonException('Paramètre "'.$param.'" incorrect.', 20);
}
}
Un test peut être marqué comme appartement à un ou plusieurs groupes en utilisant
l'annotation @group comme ceci
class MonTest extends PHPUnit_Framework_TestCase
{
/**
* @group specification
*/
public function testQuelquechose()
{
}
/**
* @group regresssion
* @group bug2204
*/
public function testAutreChose()
{
}
}
Des tests peuvent être sélectionnés pour l'exécution en se basant sur les groupes
en utilisant les options --group et --exclude-group
du lanceur de test en ligne de commandes ou en utilisant les directives respectives du
fichier de configuration XML.
L'annotation @outputBuffering peut être utilisée pour contrôler
le tampon de sortie de PHP
comme ceci
/**
* @outputBuffering enabled
*/
class MonTest extends PHPUnit_Framework_TestCase
{
// ...
}
L'annotation @outputBuffering peut également être utilisé au niveau
de la méthode de test. Ceci permet un contrôle plus fin sur le tampon de sortie :
/**
* @outputBuffering disabled
*/
class MonTest extends PHPUnit_Framework_TestCase
{
/**
* @outputBuffering enabled
*/
public function testQuiAfficheQuelqueChose()
{
// ...
}
}
Comme alternative à préfixer vos noms de méthodes de test avec
test, vous pouvez utiliser l'annotation @test
dans le bloc de documentation d'une méthode pour la marquer comme méthode de test.
/**
* @test
*/
public function balanceInitialeDoitEtre0()
{
$this->assertEquals(0, $this->ba->getBalance());
}
Les attributs d'un élément <phpunit> peuvent être
utilisés pour configurer les fonctionnalités du coeur de PHPUnit.
<phpunit backupGlobals="true"
backupStaticAttributes="false"
<!--bootstrap="/chemin/vers/amorce.php"-->
cacheTokens="true"
colors="false"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
forceCoversAnnotation="false"
mapTestClassNameToCoveredClassName="false"
printerClass="PHPUnit_TextUI_ResultPrinter"
<!--printerFile="/chemin/vers/AfficheurResultat.php"-->
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
testSuiteLoaderClass="PHPUnit_Runner_StandardTestSuiteLoader"
<!--testSuiteLoaderFile="/chemin/vers/ChargeurStandardDeSuiteDeTest.php"-->
strict="false"
verbose="false">
<!-- ... -->
</phpunit>Le fichier de configuration XML ci-dessus correspond au comportement par défaut du lanceur de tests TextUI documenté dans la section intitulée « Options de la ligne de commandes ».
Des options supplémentaires qui ne sont pas disponibles en tant qu'option de ligne de commandes sont :
convertNoticesToExceptions, convertWarningsToExceptions, convertErrorsToExceptionsPeuvent être utilisées pour désactiver la conversion automatique de toutes les erreurs, avertissement ou information de php en exception.
forceCoversAnnotation
La couverture de code ne sera enregistrée que pour les tests qui
utilisent l'annotation @covers documentée dans
la section intitulée « @covers ».
L'élément <testsuites> et son ou ses
fils <testsuite> peuvent être utilisés pour
composer une série de tests à partir des séries de test et des cas de test.
<testsuites>
<testsuite name="Ma suite de tests">
<directory>/chemin/vers/fichiers *Test.php</directory>
<file>/chemin/vers/MonTest.php</file>
<exclude>/chemin/a/exclure</exclude>
</testsuite>
</testsuites>
En utilisant les attributs phpVersion et
phpVersionOperator, une version requise de PHP
peut être indiquée. L'exemple ci-dessous ne va ajouter que
les fichiers /chemin/vers/*Test.php et
/chemin/vers/MonTest.php si la version de PHP est
au moins 5.3.0.
<testsuites>
<testsuite name="Ma suite de tests">
<directory suffix="Test.php" phpVersion="5.3.0" phpVersionOperator=">=">/chemin/vers/fichiers</directory>
<file phpVersion="5.3.0" phpVersionOperator=">=">/chemin/vers/MonTest.php</file>
</testsuite>
</testsuites>
L'attribut phpVersionOperator est facultatif et vaut par
défaut >=.
L'élément <groups> et ses fils
<include>,
<exclude> et
<group> peuvent être utilisés pour choisir
des groupes de tests depuis une série de tests qui doivent (ou ne doivent pas)
être exécutés.
<groups>
<include>
<group>nom</group>
</include>
<exclude>
<group>nom</group>
</exclude>
</groups>La configuration XML ci-dessus revient à appeler le lanceur de test TextUI avec les options suivantes:
--group nom
--exclude-group nom
L'élément <filter> et ses fils peuvent être
utilisés pour configurer les listes noires et les listes blanches pour les rapports
de couverture de code.
<filter>
<blacklist>
<directory suffix=".php">/chemin/vers/fichiers</directory>
<file>/chemin/vers/fichier</file>
<exclude>
<directory suffix=".php">/chemin/vers/fichiers</directory>
<file>/chemin/vers/fichier</file>
</exclude>
</blacklist>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory suffix=".php">/chemin/vers/fichiers</directory>
<file>/chemin/vers/fichier</file>
<exclude>
<directory suffix=".php">/chemin/vers/fichiers</directory>
<file>/chemin/vers/fichier</file>
</exclude>
</whitelist>
</filter>
L'élément <logging> et ses fils
<log> peuvent être utilisés pour configurer
la journalisation de l'exécution des tests.
<logging>
<log type="coverage-html" target="/tmp/report" charset="UTF-8"
yui="true" highlight="false"
lowUpperBound="35" highLowerBound="70"/>
<log type="coverage-clover" target="/tmp/coverage.xml"/>
<log type="coverage-php" target="/tmp/coverage.serialized"/>
<log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/>
<log type="json" target="/tmp/logfile.json"/>
<log type="tap" target="/tmp/logfile.tap"/>
<log type="junit" target="/tmp/logfile.xml" logIncompleteSkipped="false"/>
<log type="testdox-html" target="/tmp/testdox.html"/>
<log type="testdox-text" target="/tmp/testdox.txt"/>
</logging>La configuration XML ci-dessus revient à invoquer le lanceur de tests TextUI avec les options suivantes :
--coverage-html /tmp/report
--coverage-clover /tmp/coverage.xml
--coverage-php /tmp/coverage.serialized
--coverage-text
--log-json /tmp/logfile.json
> /tmp/logfile.txt
--log-tap /tmp/logfile.tap
--log-junit /tmp/logfile.xml
--testdox-html /tmp/testdox.html
--testdox-text /tmp/testdox.txt
Les attributs charset, yui,
highlight, lowUpperBound,
highLowerBound, logIncompleteSkipped
and showUncoveredFiles
n'ont pas d'options équivalentes pour le lanceur de tests TextUI.
charset: encodage de caractères à utiliser pour les pages html générées
yui: améliore le rapport de couverture html en utilisant la bibliothèque yui.
Par exemple, lorsque vous cliquez sur un numéro de ligne, un panneau YUI apparaît avec une liste de toutes les méthodes qui couvrent cette ligne.
highlight: Quand mis à vrai, les rapports de couverture de code bénéficient de la coloration syntaxique.
lowUpperBound: pourcentage de couverture maximum considérée comme étant faible.
highLowerBound: pourcentage de couverture minimum considérée comme étant forte.
showUncoveredFiles:
Montre tous les fichiers en liste blanche dans la sortie --coverage-text et pas seulement ceux possédant des informations de couverture.
L'élément <listeners> et ses fils
<listener> peuvent être utilisés pour brancher des
moniteurs de tests additionnels lors de l'exécution des tests.
<listeners>
<listener class="MonMoniteur" file="/optionnel/chemin/vers/MonMoniteur.php">
<arguments>
<array>
<element key="0">
<string>Sebastian</string>
</element>
</array>
<integer>22</integer>
<string>April</string>
<double>19.78</double>
<null/>
<object class="stdClass"/>
</arguments>
</listener>
</listeners>
La configuration XML ci-dessus revient à brancher l'objet
$moniteur (voir ci-dessous) à l'exécution des tests :
$moniteur = new MonMoniteur(
array('Sebastian'),
22,
'April',
19.78,
NULL,
new stdClass
);
L'élément <php> et ses fils peuvent être utilisés
pour configurer les réglages PHP, les constantes et les variables globales. Il peut
également être utilisé pour préfixer l'include_path.
<php> <includePath>.</includePath> <ini name="foo" value="bar"/> <const name="foo" value="bar"/> <var name="foo" value="bar"/> <env name="foo" value="bar"/> <post name="foo" value="bar"/> <get name="foo" value="bar"/> <cookie name="foo" value="bar"/> <server name="foo" value="bar"/> <files name="foo" value="bar"/> <request name="foo" value="bar"/> </php>
La configuration XML ci-dessus correspond au code PHP suivant :
ini_set('foo', 'bar');
define('foo', 'bar');
$GLOBALS['foo'] = 'bar';
$_ENV['foo'] = 'bar';
$_POST['foo'] = 'bar';
$_GET['foo'] = 'bar';
$_COOKIE['foo'] = 'bar';
$_SERVER['foo'] = 'bar';
$_FILES['foo'] = 'bar';
$_REQUEST['foo'] = 'bar';
L'élément <selenium> et ses fils
<browser> peuvent être utilisés pour
configurer une liste de serveurs Selenium RC.
<selenium>
<browser name="Firefox sur Linux"
browser="*firefox /usr/lib/firefox/firefox-bin"
host="ma.box.linux"
port="4444"
timeout="30000"/>
</selenium>La configuration XML ci-dessus correspond au code PHP suivant :
class WebTest extends PHPUnit_Extensions_SeleniumTestCase
{
public static $browsers = array(
array(
'name' => 'Firefox sur Linux',
'browser' => '*firefox /usr/lib/firefox/firefox-bin',
'host' => 'ma.box.linux',
'port' => 4444,
'timeout' => 30000
)
);
// ...
}[Astels2006] A New Look at Test-Driven Development. Copyright © 2006. http://blog.daveastels.com/files/BDD_Intro.pdf.
Copyright (c) 2005-2012 Sebastian Bergmann.
Cette oeuvre est soumise à la licence Creative Commons Attribution 3.0
non transposée.
Un résumé de la licence est donné ci-dessous, suivi de la version intégrale.
--------------------------------------------------------------------
Vous êtes libre de :
* partager - reproduire, distribuer et communiquer l'oeuvre
* adapter - adapter l'oeuvre
Selon les conditions suivantes:
Attribution. Vous devez attribuer l'oeuvre de la manière indiquée par l'auteur de l'oeuvre ou le titulaire des droits (mais pas d'une manière qui suggérerait qu'ils vous soutiennent ou approuvent votre utilisation de l'oeuvre).
* A chaque réutilisation ou distribution de cette oeuvre, vous
devez faire apparaître clairement au public la licence selon
laquelle elle est mise à disposition. La meilleure manière
de l'indiquer est un lien vers cette page web.
* N'importe laquelle des conditions ci-dessus peut être waived
si vous avez l'autorisation du titulaire de droits.
* Rien dans cette licence ne contrevient ou ne restreint les
droits moraux de l'auteur.
Votre droit à l'utilisation équitable et vos autres droits ne sont en aucune manière
affectés par ce qui précède.
Ceci est le résumé explicatif "lisible par les humains" du Code Juridique (la version intégrale de la licence) ci-dessous.
====================================================================
Creative Commons Legal Code
Attribution 3.0 non transposée
Creative Commons n'est pas un cabinet d'avocats et ne fournit pas de services de conseil juridique. La distribution de la présente version de ce contrat ne crée aucune relation juridique entre les parties au contrat présenté ci-après et Creative Commons. Creative Commons fournit cette offre de contrat-type en l'état, à seule fin d'information. Creative Commons ne saurait être tenu responsable des éventuels préjudices résultant du contenu ou de l'utilisation de ce contrat.
Contrat
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS
CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS
PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE
WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW
IS PROHIBITED.
BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND
AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS
LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU
THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF
SUCH TERMS AND CONDITIONS.
1. Definitions
a. "Adaptation" means a work based upon the Work, or upon the
Work and other pre-existing works, such as a translation,
adaptation, derivative work, arrangement of music or other
alterations of a literary or artistic work, or phonogram or
performance and includes cinematographic adaptations or any
other form in which the Work may be recast, transformed, or
adapted including in any form recognizably derived from the
original, except that a work that constitutes a Collection
will not be considered an Adaptation for the purpose of this
License. For the avoidance of doubt, where the Work is a
musical work, performance or phonogram, the synchronization of
the Work in timed-relation with a moving image ("synching")
will be considered an Adaptation for the purpose of this
License.
b. "Collection" means a collection of literary or artistic works,
such as encyclopedias and anthologies, or performances,
phonograms or broadcasts, or other works or subject matter
other than works listed in Section 1(f) below, which, by
reason of the selection and arrangement of their contents,
constitute intellectual creations, in which the Work is
included in its entirety in unmodified form along with one or
more other contributions, each constituting separate and
independent works in themselves, which together are assembled
into a collective whole. A work that constitutes a Collection
will not be considered an Adaptation (as defined above) for
the purposes of this License.
c. "Distribute" means to make available to the public the
original and copies of the Work or Adaptation, as appropriate,
through sale or other transfer of ownership.
d. "Licensor" means the individual, individuals, entity or
entities that offer(s) the Work under the terms of this License.
e. "Original Author" means, in the case of a literary or artistic
work, the individual, individuals, entity or entities who
created the Work or if no individual or entity can be
identified, the publisher; and in addition (i) in the case of
a performance the actors, singers, musicians, dancers, and
other persons who act, sing, deliver, declaim, play in,
interpret or otherwise perform literary or artistic works or
expressions of folklore; (ii) in the case of a phonogram the
producer being the person or legal entity who first fixes the
sounds of a performance or other sounds; and, (iii) in the
case of broadcasts, the organization that transmits the
broadcast.
f. "Work" means the literary and/or artistic work offered under
the terms of this License including without limitation any
production in the literary, scientific and artistic domain,
whatever may be the mode or form of its expression including
digital form, such as a book, pamphlet and other writing; a
lecture, address, sermon or other work of the same nature; a
dramatic or dramatico-musical work; a choreographic work or
entertainment in dumb show; a musical composition with or
without words; a cinematographic work to which are assimilated
works expressed by a process analogous to cinematography; a
work of drawing, painting, architecture, sculpture, engraving
or lithography; a photographic work to which are assimilated
works expressed by a process analogous to photography; a work
of applied art; an illustration, map, plan, sketch or three-
dimensional work relative to geography, topography,
architecture or science; a performance; a broadcast; a
phonogram; a compilation of data to the extent it is protected
as a copyrightable work; or a work performed by a variety or
circus performer to the extent it is not otherwise considered
a literary or artistic work.
g. "You" means an individual or entity exercising rights under
this License who has not previously violated the terms of
this License with respect to the Work, or who has received
express permission from the Licensor to exercise rights under
this License despite a previous violation.
h. "Publicly Perform" means to perform public recitations of the
Work and to communicate to the public those public
recitations, by any means or process, including by wire or
wireless means or public digital performances; to make
available to the public Works in such a way that members of
the public may access these Works from a place and at a place
individually chosen by them; to perform the Work to the public
by any means or process and the communication to the public of
the performances of the Work, including by public digital
performance; to broadcast and rebroadcast the Work by any
means including signs, sounds or images.
i. "Reproduce" means to make copies of the Work by any means
including without limitation by sound or visual recordings and
the right of fixation and reproducing fixations of the Work,
including storage of a protected performance or phonogram in
digital form or other electronic medium.
2. Fair Dealing Rights. Nothing in this License is intended to
reduce, limit, or restrict any uses free from copyright or rights
arising from limitations or exceptions that are provided for in
connection with the copyright protection under copyright law or
other applicable laws.
3. License Grant. Subject to the terms and conditions of this
License, Licensor hereby grants You a worldwide, royalty-free,
non-exclusive, perpetual (for the duration of the applicable
copyright) license to exercise the rights in the Work as stated
below:
a. to Reproduce the Work, to incorporate the Work into one or
more Collections, and to Reproduce the Work as incorporated
in the Collections;
b. to create and Reproduce Adaptations provided that any such
Adaptation, including any translation in any medium, takes
reasonable steps to clearly label, demarcate or otherwise
identify that changes were made to the original Work. For
example, a translation could be marked "The original work was
translated from English to Spanish," or a modification could
indicate "The original work has been modified.";
c. to Distribute and Publicly Perform the Work including as
incorporated in Collections; and,
d. to Distribute and Publicly Perform Adaptations.
e. For the avoidance of doubt:
i. Non-waivable Compulsory License Schemes. In those
jurisdictions in which the right to collect royalties
through any statutory or compulsory licensing scheme cannot
be waived, the Licensor reserves the exclusive right to
collect such royalties for any exercise by You of the
rights granted under this License;
ii. Waivable Compulsory License Schemes. In those
jurisdictions in which the right to collect royalties
through any statutory or compulsory licensing scheme can
be waived, the Licensor waives the exclusive right to
collect such royalties for any exercise by You of the
rights granted under this License; and,
iii. Voluntary License Schemes. The Licensor waives the right
to collect royalties, whether individually or, in the
event that the Licensor is a member of a collecting
society that administers voluntary licensing schemes, via
that society, from any exercise by You of the rights
granted under this License.
The above rights may be exercised in all media and formats whether
now known or hereafter devised. The above rights include the right
to make such modifications as are technically necessary to exercise
the rights in other media and formats. Subject to Section 8(f), all
rights not expressly granted by Licensor are hereby reserved.
4. Restrictions. The license granted in Section 3 above is expressly
made subject to and limited by the following restrictions:
a. You may Distribute or Publicly Perform the Work only under the
terms of this License. You must include a copy of, or the
Uniform Resource Identifier (URI) for, this License with every
copy of the Work You Distribute or Publicly Perform. You may
not offer or impose any terms on the Work that restrict the
terms of this License or the ability of the recipient of the
Work to exercise the rights granted to that recipient under
the terms of the License. You may not sublicense the Work. You
must keep intact all notices that refer to this License and to
the disclaimer of warranties with every copy of the Work You
Distribute or Publicly Perform. When You Distribute or
Publicly Perform the Work, You may not impose any effective
technological measures on the Work that restrict the ability
of a recipient of the Work from You to exercise the rights
granted to that recipient under the terms of the License. This
Section 4(a) applies to the Work as incorporated in a
Collection, but this does not require the Collection apart
from the Work itself to be made subject to the terms of this
License. If You create a Collection, upon notice from any
Licensor You must, to the extent practicable, remove from the
Collection any credit as required by Section 4(b), as
requested. If You create an Adaptation, upon notice from any
Licensor You must, to the extent practicable, remove from the
Adaptation any credit as required by Section 4(b), as requested.
b. If You Distribute, or Publicly Perform the Work or any
Adaptations or Collections, You must, unless a request has
been made pursuant to Section 4(a), keep intact all copyright
notices for the Work and provide, reasonable to the medium or
means You are utilizing: (i) the name of the Original Author
(or pseudonym, if applicable) if supplied, and/or if the
Original Author and/or Licensor designate another party or
parties (e.g., a sponsor institute, publishing entity,
journal) for attribution ("Attribution Parties") in Licensor's
copyright notice, terms of service or by other reasonable
means, the name of such party or parties; (ii) the title of
the Work if supplied; (iii) to the extent reasonably
practicable, the URI, if any, that Licensor specifies to be
associated with the Work, unless such URI does not refer to
the copyright notice or licensing information for the Work;
and (iv), consistent with Section 3(b), in the case of an
Adaptation, a credit identifying the use of the Work in the
Adaptation (e.g., "French translation of the Work by Original
Author," or "Screenplay based on original Work by Original
Author"). The credit required by this Section 4 (b) may be
implemented in any reasonable manner; provided, however, that
in the case of a Adaptation or Collection, at a minimum such
credit will appear, if a credit for all contributing authors
of the Adaptation or Collection appears, then as part of these
credits and in a manner at least as prominent as the credits
for the other contributing authors. For the avoidance of
doubt, You may only use the credit required by this Section
for the purpose of attribution in the manner set out above
and, by exercising Your rights under this License, You may not
implicitly or explicitly assert or imply any connection with,
sponsorship or endorsement by the Original Author, Licensor
and/or Attribution Parties, as appropriate, of You or Your use
of the Work, without the separate, express prior written
permission of the Original Author, Licensor and/or
Attribution Parties.
c. Except as otherwise agreed in writing by the Licensor or as
may be otherwise permitted by applicable law, if You
Reproduce, Distribute or Publicly Perform the Work either by
itself or as part of any Adaptations or Collections, You must
not distort, mutilate, modify or take other derogatory action
in relation to the Work which would be prejudicial to the
Original Author's honor or reputation. Licensor agrees that in
those jurisdictions (e.g. Japan), in which any exercise of the
right granted in Section 3(b) of this License (the right to
make Adaptations) would be deemed to be a distortion,
mutilation, modification or other derogatory action
prejudicial to the Original Author's honor and reputation, the
Licensor will waive or not assert, as appropriate, this
Section, to the fullest extent permitted by the applicable
national law, to enable You to reasonably exercise Your right
under Section 3(b) of this License (right to make Adaptations)
but not otherwise.
5. Representations, Warranties and Disclaimer
UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING,
LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR
WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED,
STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF
TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE,
NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT
DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF
IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY
APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY
LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE
OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF
THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY
OF SUCH DAMAGES.
7. Termination
a. This License and the rights granted hereunder will terminate
automatically upon any breach by You of the terms of this
License. Individuals or entities who have received Adaptations
or Collections from You under this License, however, will not
have their licenses terminated provided such individuals or
entities remain in full compliance with those licenses.
Sections 1, 2, 5, 6, 7, and 8 will survive any termination of
this License.
b. Subject to the above terms and conditions, the license granted
here is perpetual (for the duration of the applicable
copyright in the Work). Notwithstanding the above, Licensor
reserves the right to release the Work under different license
terms or to stop distributing the Work at any time; provided,
however that any such election will not serve to withdraw this
License (or any other license that has been, or is required to
be, granted under the terms of this License), and this License
will continue in full force and effect unless terminated as
stated above.
8. Miscellaneous
a. Each time You Distribute or Publicly Perform the Work or a
Collection, the Licensor offers to the recipient a license to
the Work on the same terms and conditions as the license
granted to You under this License.
b. Each time You Distribute or Publicly Perform an Adaptation,
Licensor offers to the recipient a license to the original
Work on the same terms and conditions as the license granted
to You under this License.
c. If any provision of this License is invalid or unenforceable
under applicable law, it shall not affect the validity or
enforceability of the remainder of the terms of this License,
and without further action by the parties to this agreement,
such provision shall be reformed to the minimum extent
necessary to make such provision valid and enforceable.
d. No term or provision of this License shall be deemed waived
and no breach consented to unless such waiver or consent shall
be in writing and signed by the party to be charged with such
waiver or consent.
e. This License constitutes the entire agreement between the
parties with respect to the Work licensed here. There are no
understandings, agreements or representations with respect to
the Work not specified here. Licensor shall not be bound by
any additional provisions that may appear in any communication
from You. This License may not be modified without the mutual
written agreement of the Licensor and You.
f. The rights granted under, and the subject matter referenced,
in this License were drafted utilizing the terminology of the
Berne Convention for the Protection of Literary and Artistic
Works (as amended on September 28, 1979), the Rome Convention
of 1961, the WIPO Copyright Treaty of 1996, the WIPO
Performances and Phonograms Treaty of 1996 and the Universal
Copyright Convention (as revised on July 24, 1971). These
rights and subject matter take effect in the relevant
jurisdiction in which the License terms are sought to be
enforced according to the corresponding provisions of the
implementation of those treaty provisions in the applicable
national law. If the standard suite of rights granted under
applicable copyright law includes additional rights not
granted under this License, such additional rights are deemed
to be included in the License; this License is not intended to
restrict the license of any rights under applicable law.
Creative Commons is not a party to this License, and makes no
warranty whatsoever in connection with the Work. Creative Commons
will not be liable to You or any party on any legal theory for any
damages whatsoever, including without limitation any general,
special, incidental or consequential damages arising in connection
to this license. Notwithstanding the foregoing two (2) sentences,
if Creative Commons has expressly identified itself as the Licensor
hereunder, it shall have all rights and obligations of Licensor.
Except for the limited purpose of indicating to the public that the
Work is licensed under the CCPL, Creative Commons does not authorize
the use by either party of the trademark "Creative Commons" or any
related trademark or logo of Creative Commons without the prior
written consent of Creative Commons. Any permitted use will be in
compliance with Creative Commons' then-current trademark usage
guidelines, as may be published on its website or otherwise made
available upon request from time to time. For the avoidance of
doubt, this trademark restriction does not form part of this
License.
Creative Commons peut être contacté à http://creativecommons.org/.
====================================================================