Prev Next

Chapitre 13. Développement dirigé par le comportement

Dans [Astels2006], Dave Astels fait le point suivant :

 

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)

    ...

Exemple du jeu de Bowling

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.7.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.

Prev Next
1. Automatiser les tests
2. Objectifs de PHPUnit
3. Installer PHPUnit
4. Ecrire des tests pour PHPUnit
Dépendances des tests
Fournisseur de données
Tester des exceptions
Tester les erreurs PHP
Tester la sortie écran
Assertions
assertArrayHasKey()
assertClassHasAttribute()
assertClassHasStaticAttribute()
assertContains()
assertContainsOnly()
assertCount()
assertEmpty()
assertEqualXMLStructure()
assertEquals()
assertFalse()
assertFileEquals()
assertFileExists()
assertGreaterThan()
assertGreaterThanOrEqual()
assertInstanceOf()
assertInternalType()
assertJsonFileEqualsJsonFile()
assertJsonStringEqualsJsonFile()
assertJsonStringEqualsJsonString()
assertLessThan()
assertLessThanOrEqual()
assertNull()
assertObjectHasAttribute()
assertRegExp()
assertStringMatchesFormat()
assertStringMatchesFormatFile()
assertSame()
assertSelectCount()
assertSelectEquals()
assertSelectRegExp()
assertStringEndsWith()
assertStringEqualsFile()
assertStringStartsWith()
assertTag()
assertThat()
assertTrue()
assertXmlFileEqualsXmlFile()
assertXmlStringEqualsXmlFile()
assertXmlStringEqualsXmlString()
5. Le lanceur de tests en ligne de commandes
Options de la ligne de commandes
6. Fixtures
Plus de setUp() que de tearDown()
Variantes
Partager les Fixtures
Etat global
7. Organiser les tests
Composer une suite de tests en utilisant le système de fichiers
Composer une suite de tests en utilisant la configuration XML
8. Tester des bases de données
Systèmes gérés pour tester des bases de données
Difficultés pour tester les bases de données
Les quatre phases d'un test de base de données
1. Nettoyer la base de données
2. Configurer les fixtures
3–5. Exécuter les tests, vérifier les résultats et nettoyer
Configuration d'un cas de test de base de données PHPUnit
Implémenter getConnection()
Implémenter getDataSet()
Qu'en est-il du schéma de base de données (DDL)?
Astuce: utilisez votre propre cas de tests abstrait de base de données
Comprendre DataSets et DataTables
Implémentations disponibles
Attention aux clefs étrangères
Implementer vos propres DataSets/DataTables
L'API de connexion
API d'assertion de base de données
Faire une assertion sur le nombre de lignes d'une table
Faire une assertion sur l'état d'une table
Faire une assertion sur le résultat d'une requête
Faire une assertion sur l'état de plusieurs tables
Foire aux questions
PHPUnit va-t'il (re-)créer le schéma de base de données pour chaque test ?
Suis-je obligé d'utiliser PDO dans mon application pour que l'extension de base de données fonctionne ?
Que puis-je faire quand j'obtiens une erreur « Too much Connections (Trop de connexions) » ?
Comment gérer les valeurs NULL avec les DataSets au format XML à plat / CSV ?
9. Tests incomplets et sautés
Tests incomplets
Sauter des tests
Sauter des tests en utilisant @requires
10. Doublure de test
Bouchons
Objets simulacres (Mock Objects)
Bouchon et simulacre pour Web Services
Simuler le système de fichiers
11. Pratiques de test
Pendant le développement
Pendant le débogage
12. Développement dirigé par les tests
Exemple du compte bancaire
13. Développement dirigé par le comportement
Exemple du jeu de Bowling
14. Analyse de couverture de code
Spécifier les méthodes couvertes
Ignorer des blocs de code
Inclure et exclure des fichiers
Cas limites
15. Autres utilisations des tests
Documentation agile
Tests transverses à l'équipe
16. Générateur de squelette
Générer un squelettre de classe de cas de test
Générer un squelette de classe à partir d'une classe de cas de test
17. PHPUnit et Selenium
Selenium Server
Installation
PHPUnit_Extensions_Selenium2TestCase
PHPUnit_Extensions_SeleniumTestCase
18. Journalisation
Résultats de test (XML)
Résultats de test (TAP)
Résultats de test (JSON)
Couverture de code (XML)
Couverture de code (TEXTE)
19. Etendre PHPUnit
Sous-classe PHPUnit_Framework_TestCase
Ecrire des assertions personnalisées
Implémenter PHPUnit_Framework_TestListener
Sous classer PHPUnit_Extensions_TestDecorator
Implémenter PHPUnit_Framework_Test
A. Assertions
B. Annotations
@author
@backupGlobals
@backupStaticAttributes
@codeCoverageIgnore*
@covers
@coversNothing
@dataProvider
@depends
@expectedException
@expectedExceptionCode
@expectedExceptionMessage
@group
@outputBuffering
@requires
@runTestsInSeparateProcesses
@runInSeparateProcess
@test
@testdox
@ticket
C. Le fichier de configuration Configuration
PHPUnit
Série de tests
Groupes
Inclure et exclure des fichiers de la couverture de code
Journalisation
Moniteurs de tests
Configurer les réglages de PHP INI, les constantes et les variables globales
Configurer les navigateurs pour Selenium RC
D. Index
Index
E. Bibliographie
F. Copyright