feat: add videodb media index with Docker stack
- Add videodb PHP/MySQL media collection manager (Blu-ray, DVD, CD) - Dockerfile: PHP 8.1 + Apache with GD/mysqli/exif extensions - docker-compose.yml: app on port 6761 + MySQL 8.0 with health checks - docker-entrypoint.sh: auto-generates config.inc.php from env vars, waits for MySQL, initializes DB schema idempotently - init-db.php: CLI schema installer using app's own prefix_query() logic - Persistent volumes for DB, cache, and cover images Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
933
videodb/test/simpletest/docs/fr/mock_objects_documentation.html
Normal file
933
videodb/test/simpletest/docs/fr/mock_objects_documentation.html
Normal file
@@ -0,0 +1,933 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Documentation SimpleTest : les objets fantaise</title>
|
||||
<link rel="stylesheet" type="text/css" href="docs.css" title="Styles">
|
||||
</head>
|
||||
<body>
|
||||
<div class="menu_back"><div class="menu">
|
||||
<a href="index.html">SimpleTest</a>
|
||||
|
|
||||
<a href="overview.html">Overview</a>
|
||||
|
|
||||
<a href="unit_test_documentation.html">Unit tester</a>
|
||||
|
|
||||
<a href="group_test_documentation.html">Group tests</a>
|
||||
|
|
||||
<a href="mock_objects_documentation.html">Mock objects</a>
|
||||
|
|
||||
<a href="partial_mocks_documentation.html">Partial mocks</a>
|
||||
|
|
||||
<a href="reporter_documentation.html">Reporting</a>
|
||||
|
|
||||
<a href="expectation_documentation.html">Expectations</a>
|
||||
|
|
||||
<a href="web_tester_documentation.html">Web tester</a>
|
||||
|
|
||||
<a href="form_testing_documentation.html">Testing forms</a>
|
||||
|
|
||||
<a href="authentication_documentation.html">Authentication</a>
|
||||
|
|
||||
<a href="browser_documentation.html">Scriptable browser</a>
|
||||
</div></div>
|
||||
<h1>Documentation sur les objets fantaisie</h1>
|
||||
This page...
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#what">Que sont les objets fantaisie ?</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#creation">Créer des objets fantaisie</a>.
|
||||
</li>
|
||||
<li>
|
||||
<a href="#expectations">Les objets fantaisie en tant que critiques</a> avec les attentes.
|
||||
</li>
|
||||
</ul>
|
||||
<div class="content">
|
||||
<h2>
|
||||
<a class="target" name="what"></a>Que sont les objets fantaisie ?</h2>
|
||||
<p>
|
||||
Les objets fantaisie - ou "mock objects" en anglais -
|
||||
ont deux rôles pendant un scénario de test : acteur et critique.
|
||||
</p>
|
||||
<p>
|
||||
Le comportement d'acteur est celui de simuler
|
||||
des objets difficiles à initialiser ou trop consommateurs
|
||||
en temps pendant un test.
|
||||
Le cas classique est celui de la connexion à une base de données.
|
||||
Mettre sur pied une base de données de test au lancement
|
||||
de chaque test ralentirait considérablement les tests
|
||||
et en plus exigerait l'installation d'un moteur
|
||||
de base de données ainsi que des données sur la machine de test.
|
||||
Si nous pouvons simuler la connexion
|
||||
et renvoyer des données à notre guise
|
||||
alors non seulement nous gagnons en pragmatisme
|
||||
sur les tests mais en sus nous pouvons nourrir
|
||||
notre base avec des données falsifiées
|
||||
et voir comment il répond. Nous pouvons
|
||||
simuler une base de données en suspens ou
|
||||
d'autres cas extrêmes sans avoir à créer
|
||||
une véritable panne de base de données.
|
||||
En d'autres termes nous pouvons gagner
|
||||
en contrôle sur l'environnement de test.
|
||||
</p>
|
||||
<p>
|
||||
Si les objets fantaisie ne se comportaient que comme
|
||||
des acteurs alors on les connaîtrait sous le nom de
|
||||
<a href="server_stubs_documentation.html">bouchons serveur</a>.
|
||||
Il s'agissait originairement d'un patron de conception
|
||||
identifié par Robert Binder (<a href="">Testing
|
||||
object-oriented systems</a>: models, patterns, and tools,
|
||||
Addison-Wesley) en 1999.
|
||||
</p>
|
||||
<p>
|
||||
Un bouchon serveur est une simulation d'un objet ou d'un composant.
|
||||
Il doit remplacer exactement un composant dans un système
|
||||
en vue d'effectuer des tests ou un prototypage,
|
||||
tout en restant ultra-léger.
|
||||
Il permet aux tests de s'exécuter plus rapidement, ou
|
||||
si la classe simulée n'a pas encore été écrite,
|
||||
de se lancer tout court.
|
||||
</p>
|
||||
<p>
|
||||
Cependant non seulement les objets fantaisie jouent
|
||||
un rôle (en fournissant à la demande les valeurs requises)
|
||||
mais en plus ils sont aussi sensibles aux messages qui
|
||||
leur sont envoyés (par le biais d'attentes).
|
||||
En posant les paramètres attendus d'une méthode
|
||||
ils agissent comme des gardiens :
|
||||
un appel sur eux doit être réalisé correctement.
|
||||
Si les attentes ne sont pas atteintes ils nous épargnent
|
||||
l'effort de l'écriture d'une assertion de test avec
|
||||
échec en réalisant cette tâche à notre place.
|
||||
</p>
|
||||
<p>
|
||||
Dans le cas d'une connexion à une base de données
|
||||
imaginaire ils peuvent tester si la requête, disons SQL,
|
||||
a bien été formé par l'objet qui utilise cette connexion.
|
||||
Mettez-les sur pied avec des attentes assez précises
|
||||
et vous verrez que vous n'aurez presque plus d'assertion à écrire manuellement.
|
||||
</p>
|
||||
|
||||
<h2>
|
||||
<a class="target" name="creation"></a>Créer des objets fantaisie</h2>
|
||||
<p>
|
||||
Tout ce dont nous avons besoin est une classe existante ou une interface,
|
||||
par exemple une connexion à la base de données qui ressemblerait à...
|
||||
<pre>
|
||||
<strong>class DatabaseConnection {
|
||||
function DatabaseConnection() { }
|
||||
function query($sql) { }
|
||||
function selectQuery($sql) { }
|
||||
}</strong>
|
||||
</pre>
|
||||
Pour en créer sa version fantaisie nous devons juste
|
||||
lancer le générateur...
|
||||
<pre>
|
||||
require_once('simpletest/autorun.php');
|
||||
require_once('database_connection.php');
|
||||
|
||||
<strong>Mock::generate('DatabaseConnection');</strong>
|
||||
</pre>
|
||||
Ce code génère une classe clone appelée <span class="new_code">MockDatabaseConnection</span>.
|
||||
Cette nouvelle classe lui ressemble en tout point,
|
||||
sauf qu'elle ne fait rien du tout.
|
||||
</p>
|
||||
<p>
|
||||
Cette nouvelle classe est génératlement
|
||||
une sous-classe de <span class="new_code">DatabaseConnection</span>.
|
||||
Malheureusement, il n'y as aucun moyen de créer une version fantaisie
|
||||
d'une classe avec une méthode <span class="new_code">final</span> sans avoir
|
||||
une version fonctionnelle de cette méthode.
|
||||
Ce n'est pas pas très satisfaisant.
|
||||
Si la cible est une interface ou si les méthodes <span class="new_code">final</span>
|
||||
existent dans la classe cible, alors une toute nouvelle classe
|
||||
est créée, elle implémente juste les même interfaces.
|
||||
Si vous essayer de faire passer cette classe à travers un indice de type
|
||||
qui spécifie le véritable nom de l'ancienne classe, alors il échouera.
|
||||
Du code qui forcerait un indice de type tout en utilisant
|
||||
des méthodes <span class="new_code">final</span> ne pourrait probablement pas être
|
||||
testé efficacement avec des objets fantaisie.
|
||||
</p>
|
||||
<p>
|
||||
Si vous voulez voir le code généré, il suffit de faire un <span class="new_code">print</span>
|
||||
de la sortie de <span class="new_code">Mock::generate()</span>.
|
||||
VOici le code généré pour la classe <span class="new_code">DatabaseConnection</span>
|
||||
à la place de son interface...
|
||||
<pre>
|
||||
class MockDatabaseConnection extends DatabaseConnection {
|
||||
public $mock;
|
||||
protected $mocked_methods = array('databaseconnection', 'query', 'selectquery');
|
||||
|
||||
function MockDatabaseConnection() {
|
||||
$this->mock = new SimpleMock();
|
||||
$this->mock->disableExpectationNameChecks();
|
||||
}
|
||||
...
|
||||
function DatabaseConnection() {
|
||||
$args = func_get_args();
|
||||
$result = &$this->mock->invoke("DatabaseConnection", $args);
|
||||
return $result;
|
||||
}
|
||||
function query($sql) {
|
||||
$args = func_get_args();
|
||||
$result = &$this->mock->invoke("query", $args);
|
||||
return $result;
|
||||
}
|
||||
function selectQuery($sql) {
|
||||
$args = func_get_args();
|
||||
$result = &$this->mock->invoke("selectQuery", $args);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
Votre sortie dépendra quelque peu de votre version précise de SimpleTest.
|
||||
</p>
|
||||
<p>
|
||||
En plus des méthodes d'origine de la classe, vous en verrez d'autres
|
||||
pour faciliter les tests.
|
||||
Nous y reviendrons.
|
||||
</p>
|
||||
<p>
|
||||
Nous pouvons désormais créer des instances de
|
||||
cette nouvelle classe à l'intérieur même de notre scénario de test...
|
||||
<pre>
|
||||
require_once('simpletest/autorun.php');
|
||||
require_once('database_connection.php');
|
||||
|
||||
Mock::generate('DatabaseConnection');
|
||||
|
||||
class MyTestCase extends UnitTestCase {
|
||||
|
||||
function testSomething() {
|
||||
<strong>$connection = new MockDatabaseConnection();</strong>
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
La version fantaisie contient toutles méthodes de l'originale.
|
||||
En outre, tous les indices de type seront préservés.
|
||||
Dison que nos méthodes de requête attend un objet <span class="new_code">Query</span>...
|
||||
<pre>
|
||||
<strong>class DatabaseConnection {
|
||||
function DatabaseConnection() { }
|
||||
function query(Query $query) { }
|
||||
function selectQuery(Query $query) { }
|
||||
}</strong>
|
||||
</pre>
|
||||
Si nous lui passons le mauvais type d'objet
|
||||
ou même pire un non-objet...
|
||||
<pre>
|
||||
class MyTestCase extends UnitTestCase {
|
||||
|
||||
function testSomething() {
|
||||
$connection = new MockDatabaseConnection();
|
||||
$connection->query('insert into accounts () values ()');
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
...alors le code renverra une violation de typage,
|
||||
exactement comme on aurait pû s'y attendre
|
||||
avec la classe d'origine.
|
||||
</p>
|
||||
<p>
|
||||
Si la version fantaisie implémente bien toutes les méthodes
|
||||
de l'originale, elle renvoit aussi systématiquement <span class="new_code">null</span>.
|
||||
Et comme toutes les méthodes qui renvoient toujours <span class="new_code">null</span>
|
||||
ne sont pas très utiles, nous avons besoin de leur faire dire
|
||||
autre chose...
|
||||
</p>
|
||||
|
||||
<h2>
|
||||
<a class="target" name="bouchon"></a>Objets fantaisie en action</h2>
|
||||
<p>
|
||||
Changer le <span class="new_code">null</span> renvoyé par la méthode fantaisie
|
||||
en autre chose est assez facile...
|
||||
<pre>
|
||||
<strong>$connection->returns('query', 37)</strong>
|
||||
</pre>
|
||||
Désormais à chaque appel de <span class="new_code">$connection->query()</span>
|
||||
nous obtenons un résultat de 37.
|
||||
Il n'y a rien de particulier à cette valeur de 37.
|
||||
Cette valeur de retour peut être aussi compliqué que nécessaire.
|
||||
</p>
|
||||
<p>
|
||||
Ici les paramètres ne sont pas significatifs,
|
||||
nous aurons systématiquement la même valeur en retour
|
||||
une fois initialisés de la sorte.
|
||||
Cela pourrait ne pas ressembler à une copie convaincante
|
||||
de notre connexion à la base de données, mais pour
|
||||
la demi-douzaine de lignes de code de notre méthode de test
|
||||
c'est généralement largement assez.
|
||||
</p>
|
||||
<p>
|
||||
Sauf que les choses ne sont pas toujours si simples.
|
||||
Les itérateurs sont un problème récurrent, si par exemple
|
||||
renvoyer systématiquement la même valeur entraine
|
||||
une boucle infinie dans l'objet testé.
|
||||
Pour ces cas-là, nous avons besoin d'une séquence de valeurs.
|
||||
Supposons que nous avons un itérateur simple qui ressemble à...
|
||||
<pre>
|
||||
class Iterator {
|
||||
function Iterator() { }
|
||||
function next() { }
|
||||
}
|
||||
</pre>
|
||||
Il s'agit là de l'itérateur le plus basique que nous puissions imaginer.
|
||||
Supponsons que cet itérateur ne renvoit que du texte
|
||||
jusqu'à ce qu'il atteigne la fin et qu'il renvoie alors un false,
|
||||
nous pouvons le simuler avec...
|
||||
<pre>
|
||||
Mock::generate('Iterator');
|
||||
|
||||
class IteratorTest extends UnitTestCase() {
|
||||
|
||||
function testASequence() {<strong>
|
||||
$iterator = new MockIterator();
|
||||
$iterator->returns('next', false);
|
||||
$iterator->returnsAt(0, 'next', 'First string');
|
||||
$iterator->returnsAt(1, 'next', 'Second string');</strong>
|
||||
...
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
Quand <span class="new_code">next()</span> est appelé par le <span class="new_code">MockIterator</span>
|
||||
il commencera par renvoyer "First string",
|
||||
au deuxième passage "Second string" sera renvoyé
|
||||
et sur n'importe quel autre appel <span class="new_code">false</span> sera renvoyé
|
||||
à son tour.
|
||||
Les valeurs renvoyées les unes après les autres auront la priorité
|
||||
sur la valeur constante.
|
||||
Cette dernière est la valeur par défaut en quelque sorte.
|
||||
</p>
|
||||
<p>
|
||||
Une autre situation délicate serait une opération
|
||||
<span class="new_code">get()</span> surchargée.
|
||||
Un exemple serait un conteneur d'information avec des pairs clef/valeur.
|
||||
Nous partons cette fois d'une classe de configuration telle...
|
||||
<pre>
|
||||
class Configuration {
|
||||
function Configuration() { ... }
|
||||
function get($key) { ... }
|
||||
}
|
||||
</pre>
|
||||
C'est une situation courante pour utiliser des objets fantaisie,
|
||||
étant donné que la véritable configuration sera différente
|
||||
d'une machine à l'autre et parfois même d'un test à l'autre.
|
||||
Cependant un problème apparaît quand toutes les données passent
|
||||
par la méthode <span class="new_code">get()</span> et que nous souhaitons
|
||||
quand même des résultats différents pour des clefs différentes.
|
||||
Par chance les objets fantaisie ont un système de filtre...
|
||||
<pre>
|
||||
<strong>$config = &new MockConfiguration();
|
||||
$config->returns('get', 'primary', array('db_host'));
|
||||
$config->returns('get', 'admin', array('db_user'));
|
||||
$config->returns('get', 'secret', array('db_password'));</strong>
|
||||
</pre>
|
||||
Le dernier paramètre est une liste d'arguements
|
||||
pour vérifier une correspondance.
|
||||
Dans ce cas, nous essayons de faire correspondre un argument
|
||||
qui se trouve être la clef de recherche.
|
||||
Désormais quand l'objet fantaisie voit
|
||||
sa méthode <span class="new_code">get()</span> invoquée...
|
||||
<pre>
|
||||
$config->get('db_user')
|
||||
</pre>
|
||||
...il renverra "admin".
|
||||
Il trouve cette valeur en essayant de faire correspondre
|
||||
l'argument reçu à ceux de ses propres listes : dès qu'une
|
||||
correspondance complète est trouvé, il s'arrête.
|
||||
</p>
|
||||
<p>
|
||||
Vous pouvez préciser un argument par défaut via...
|
||||
<pre><strong>
|
||||
$config->returns('get', false, array('*'));</strong>
|
||||
</pre>
|
||||
Ce n'est pas la même chose que de définir la valeur renvoyée
|
||||
sans aucun arguement...
|
||||
<pre><strong>
|
||||
$config->returns('get', false);</strong>
|
||||
</pre>
|
||||
Dans le premier cas, il acceptera n'importe quel argument,
|
||||
mais il en faut au moins un.
|
||||
Dans le deuxième cas, n'importe quel nombre d'arguments
|
||||
fera l'affaire and il agit comme un attrape-tout après
|
||||
toutes les autres vérifications.
|
||||
Notez que si - dans le premier cas - nous ajoutons
|
||||
d'autres options avec paramètre unique après le joker,
|
||||
alors elle seront ignorés puisque le joker fera
|
||||
la première correspondance.
|
||||
Avec des listes complexes de paramètres, l'ordre devient
|
||||
important au risque de voir la correspondance souhaitée
|
||||
masqué par un joker apparu plus tôt.
|
||||
Déclarez les plus spécifiques d'abord si vous n'êtes pas sûr.
|
||||
</p>
|
||||
<p>
|
||||
Il y a des moments où vous souhaitez qu'une référence
|
||||
bien spécifique soit servie par l'objet fantaisie plutôt
|
||||
qu'une copie.
|
||||
C'est plutôt rare pour dire le moins, mais vous pourriez
|
||||
être en train de simuler un conteneur qui détiendrait
|
||||
des primitives, tels des chaînes de caractères.
|
||||
Par exemple.
|
||||
<pre>
|
||||
class Pad {
|
||||
function Pad() { }
|
||||
function &note($index) { }
|
||||
}
|
||||
</pre>
|
||||
Dans ce cas, vous pouvez définir une référence dans la liste
|
||||
des valeurs retournées par l'objet fantaisie...
|
||||
<pre>
|
||||
function testTaskReads() {
|
||||
$note = 'Buy books';
|
||||
$pad = new MockPad();
|
||||
$vector-><strong>returnsByReference(</strong>'note', $note, array(3)<strong>)</strong>;
|
||||
$task = new Task($pad);
|
||||
...
|
||||
}
|
||||
</pre>
|
||||
Avec cet assemblage vous savez qu'à chaque fois
|
||||
que <span class="new_code">$pad->note(3)</span> est appelé
|
||||
il renverra toujours la même <span class="new_code">$note</span>,
|
||||
même si celle-ci est modifiée.
|
||||
</p>
|
||||
<p>
|
||||
Ces trois facteurs, timing, paramètres et références,
|
||||
peuvent être combinés orthogonalement.
|
||||
Par exemple...
|
||||
<pre>
|
||||
$buy_books = 'Buy books';
|
||||
$write_code = 'Write code';
|
||||
$pad = new MockPad();
|
||||
$vector-><strong>returnsByReferenceAt(0, 'note', $buy_books, array('*', 3));</strong>
|
||||
$vector-><strong>returnsByReferenceAt(1, 'note', $write_code, array('*', 3));</strong>
|
||||
</pre>
|
||||
Cela renverra une référence à <span class="new_code">$buy_books</span> et
|
||||
ensuite à <span class="new_code">$write_code</span>, mais seuleent si deux paramètres
|
||||
sont utilisés, le deuxième devant être l'entier 3.
|
||||
Cela devrait couvrir la plupart des situations.
|
||||
</p>
|
||||
<p>
|
||||
Un dernier cas délicat reste : celui d'un objet créant
|
||||
un autre objet, plus connu sous le patron de conception "fabrique"
|
||||
(ou "factory").
|
||||
Supponsons qu'à la réussite d'une requête à
|
||||
notre base de données imaginaire, un jeu d'enregistrements
|
||||
est renvoyé sous la forme d'un itérateur, où chaque appel
|
||||
au <span class="new_code">next()</span> sur notre itérateur donne une ligne
|
||||
avant de s'arrêtre avec un false.
|
||||
Cela semble à un cauchemar à simuler, alors qu'en fait un objet
|
||||
fantaisie peut être créé avec les mécansimes ci-dessus...
|
||||
<pre>
|
||||
Mock::generate('DatabaseConnection');
|
||||
Mock::generate('ResultIterator');
|
||||
|
||||
class DatabaseTest extends UnitTestCase {
|
||||
|
||||
function testUserFinderReadsResultsFromDatabase() {<strong>
|
||||
$result = new MockResultIterator();
|
||||
$result->returns('next', false);
|
||||
$result->returnsAt(0, 'next', array(1, 'tom'));
|
||||
$result->returnsAt(1, 'next', array(3, 'dick'));
|
||||
$result->returnsAt(2, 'next', array(6, 'harry'));
|
||||
|
||||
$connection = new MockDatabaseConnection();
|
||||
$connection->returns('selectQuery', $result);</strong>
|
||||
|
||||
$finder = new UserFinder(<strong>$connection</strong>);
|
||||
$this->assertIdentical(
|
||||
$finder->findNames(),
|
||||
array('tom', 'dick', 'harry'));
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
Désormais ce n'est que si notre <span class="new_code">$connection</span>
|
||||
est appelée par la méthode <span class="new_code">query()</span>
|
||||
que sera retourné le <span class="new_code">$result</span>,
|
||||
lui-même s'arrêtant au troisième appel
|
||||
à <span class="new_code">next()</span>.
|
||||
Ce devrait être suffisant comme information
|
||||
pour que notre classe <span class="new_code">UserFinder</span>,
|
||||
la classe effectivement testée ici,
|
||||
fasse son boulot.
|
||||
Un test très précis et toujours pas
|
||||
de base de données en vue.
|
||||
</p>
|
||||
<p>
|
||||
Nous pourrsion affiner ce test encore plus
|
||||
en insistant pour que la bonne requête
|
||||
soit envoyée...
|
||||
<pre>
|
||||
$connection->returns('selectQuery', $result, array(<strong>'select name, id from people'</strong>));
|
||||
</pre>
|
||||
Là, c'est une mauvaise idée.
|
||||
</p>
|
||||
<p>
|
||||
Niveau objet, nous avons un <span class="new_code">UserFinder</span>
|
||||
qui parle à une base de données à travers une interface géante -
|
||||
l'ensemble du SQL.
|
||||
Imaginez si nous avions écrit un grand nombre de tests
|
||||
qui dépendrait désormais de cette requête SQL précise.
|
||||
Ces requêtes pourraient changer en masse pour tout un tas
|
||||
de raisons non liés à ce test spécifique.
|
||||
Par exemple, la règle pour les quotes pourrait changer,
|
||||
un nom de table pourrait évoluer, une table de liaison pourrait
|
||||
être ajouté, etc.
|
||||
Cela entrainerait une ré-écriture de tous les tests à chaque fois
|
||||
qu'un remaniement est fait, alors même que le comportement lui
|
||||
n'a pas bougé.
|
||||
Les tests sont censés aider au remaniement, pas le bloquer.
|
||||
Pour ma part, je préfère avoir une suite de tests qui passent
|
||||
quand je fais évoluer le nom des tables.
|
||||
</p>
|
||||
<p>
|
||||
Et si vous voulez une règle, c'est toujours mieux de ne pas
|
||||
créer un objet fantaisie sur une grosse interface.
|
||||
</p>
|
||||
<p>
|
||||
Par contrast, voici le test complet...
|
||||
<pre>
|
||||
class DatabaseTest extends UnitTestCase {<strong>
|
||||
function setUp() { ... }
|
||||
function tearDown() { ... }</strong>
|
||||
|
||||
function testUserFinderReadsResultsFromDatabase() {
|
||||
$finder = new UserFinder(<strong>new DatabaseConnection()</strong>);
|
||||
$finder->add('tom');
|
||||
$finder->add('dick');
|
||||
$finder->add('harry');
|
||||
$this->assertIdentical(
|
||||
$finder->findNames(),
|
||||
array('tom', 'dick', 'harry'));
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
Ce test est immunisé contre le changement de schéma.
|
||||
Il échouera uniquement si vous changez la fonctionnalité,
|
||||
ce qui correspond bien à ce qu'un test doit faire.
|
||||
</p>
|
||||
<p>
|
||||
Il faut juste faire attention à ces méthodes <span class="new_code">setUp()</span>
|
||||
et <span class="new_code">tearDown()</span> que nous avons survolé pour l'instant.
|
||||
Elles doivent vider les tables de la base de données
|
||||
et s'assurer que le schéma est bien défini.
|
||||
Cela peut se engendrer un peu de travail supplémentaire,
|
||||
mais d'ordinaire ce type de code existe déjà - à commencer pour
|
||||
le déploiement.
|
||||
</p>
|
||||
<p>
|
||||
Il y a au moins un endroit où vous aurez besoin d'objets fantaisie :
|
||||
c'est la simulation des erreurs.
|
||||
Exemple, la base de données tombe pendant que <span class="new_code">UserFinder</span>
|
||||
fait son travail. Le <span class="new_code">UserFinder</span> se comporte-t-il bien ?
|
||||
<pre>
|
||||
class DatabaseTest extends UnitTestCase {
|
||||
|
||||
function testUserFinder() {
|
||||
$connection = new MockDatabaseConnection();<strong>
|
||||
$connection->throwOn('selectQuery', new TimedOut('Ouch!'));</strong>
|
||||
$alert = new MockAlerts();<strong>
|
||||
$alert->expectOnce('notify', 'Database is busy - please retry');</strong>
|
||||
$finder = new UserFinder($connection, $alert);
|
||||
$this->assertIdentical($finder->findNames(), array());
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
Nous avons transmis au <span class="new_code">UserFinder</span>
|
||||
un objet <span class="new_code">$alert</span>.
|
||||
Il va transmettre un certain nombre de belles notifications
|
||||
à l'interface utilisatuer en cas d'erreur.
|
||||
Nous préfèrerions éviter de saupoudrer notre code avec
|
||||
des commandes <span class="new_code">print</span> codées en dur si nous pouvons
|
||||
l'éviter.
|
||||
Emballer les moyens de sortie veut dire que nous pouvons utiliser
|
||||
ce code partout. Et cela rend notre code plus facile.
|
||||
</p>
|
||||
<p>
|
||||
Pour faire passer ce test, le finder doit écrire un message sympathique
|
||||
et compréhensible à l'<span class="new_code">$alert</span>, plutôt que de propager
|
||||
l'exception. Jusque là, tout va bien.
|
||||
</p>
|
||||
<p>
|
||||
Comment faire en sorte que la <span class="new_code">DatabaseConnection</span> fantaisie
|
||||
soulève une exception ?
|
||||
Nous la générons avec la méthode <span class="new_code">throwOn</span> sur l'objet fantaisie.
|
||||
</p>
|
||||
<p>
|
||||
Enfin, que se passe-t-il si la méthode voulue pour la simulation
|
||||
n'existe pas encore ?
|
||||
Si vous définissez une valeur de retour sur une méthode absente,
|
||||
alors SimpleTest vous répondra avec une erreur.
|
||||
Et si vous utilisez <span class="new_code">__call()</span> pour simuler
|
||||
des méthodes dynamiques ?
|
||||
</p>
|
||||
<p>
|
||||
Les objets avec des interfaces dynamiques, avec <span class="new_code">__call</span>,
|
||||
peuvent être problématiques dans l'implémentation courante
|
||||
des objets fantaisie.
|
||||
Vous pouvez en créer un autour de la méthode <span class="new_code">__call()</span>
|
||||
mais c'est très laid.
|
||||
Et pourquoi un test devrait connaître quelque chose avec un niveau
|
||||
si bas dans l'implémentation. Il n'a besoin que de simuler l'appel.
|
||||
</p>
|
||||
<p>
|
||||
Il y a bien moyen de contournement : ajouter des méthodes complémentaires
|
||||
à l'objet fantaisie à la génération.
|
||||
<pre>
|
||||
<strong>Mock::generate('DatabaseConnection', 'MockDatabaseConnection', array('setOptions'));</strong>
|
||||
</pre>
|
||||
Dans une longue suite de tests cela pourrait entraîner des problèmes,
|
||||
puisque vous avez probablement déjà une version fantaisie
|
||||
de la classe appellée <span class="new_code">MockDatabaseConnection</span>
|
||||
sans les méthodes complémentaires.
|
||||
Le générateur de code ne générera pas la version fantaisie de la classe
|
||||
s'il en existe déjà une version avec le même nom.
|
||||
Il vous deviendra impossible de déterminer où est passée votre méthode
|
||||
si une autre définition a été lancé au préalable.
|
||||
</p>
|
||||
<p>
|
||||
Pour pallier à ce problème, SimpleTest vous permet de choisir
|
||||
n'importe autre nom pour la nouvelle classe au moment même où
|
||||
vous ajouter les méthodes complémentaires.
|
||||
<pre>
|
||||
Mock::generate('DatabaseConnection', <strong>'MockDatabaseConnectionWithOptions'</strong>, array('setOptions'));
|
||||
</pre>
|
||||
Ici l'objet fantaisie se comportera comme si
|
||||
<span class="new_code">setOptions()</span> existait bel et bien
|
||||
dans la classe originale.
|
||||
</p>
|
||||
<p>
|
||||
Les objets fantaisie ne peuvent être utilisés qu'à l'intérieur
|
||||
des scénarios de test, étant donné qu'à l'apparition d'une attente
|
||||
ils envoient des messages directement au scénario de test courant.
|
||||
Les créer en dehors d'un scénario de test entraînera une erreur
|
||||
de run time quand une attente est déclenchée et qu'il n'y a pas
|
||||
de scénario de test en cours pour recevoir le message.
|
||||
Nous pouvons désormais couvrir ces attentes.
|
||||
</p>
|
||||
|
||||
<h2>
|
||||
<a class="target" name="expectations"></a>Objets fantaisie en tant que critiques</h2>
|
||||
<p>
|
||||
Même si les bouchons serveur isolent vos tests des perturbations
|
||||
du monde réel, ils n'apportent que le moitié des bénéfices possibles.
|
||||
Vous pouvez obtenir une classe de test qui reçoive les bons messages,
|
||||
mais cette nouvelle classe envoie-t-elle les bons ?
|
||||
Le tester peut devenir très bordélique sans
|
||||
une librairie d'objets fantaise.
|
||||
</p>
|
||||
<p>
|
||||
Voici un exemple, prenons une classe <span class="new_code">PageController</span>
|
||||
toute simple qui traitera un formulaire de paiement
|
||||
par carte bleue...
|
||||
<pre>
|
||||
class PaymentForm extends PageController {
|
||||
function __construct($alert, $payment_gateway) { ... }
|
||||
function makePayment($request) { ... }
|
||||
}
|
||||
</pre>
|
||||
Cette classe a besoin d'un <span class="new_code">PaymentGateway</span>
|
||||
pour parler concrètement à la banque.
|
||||
Il utilise aussi un objet <span class="new_code">Alert</span>
|
||||
pour gérer les erreurs.
|
||||
Cette dernière classe parle à la page ou au template.
|
||||
Elle est responsable de l'affichage du message d'erreur
|
||||
et de la mise en valeur de tout champ du formulaire
|
||||
qui serait incorrecte.
|
||||
</p>
|
||||
<p>
|
||||
Elle pourrait ressembler à...
|
||||
<pre>
|
||||
class Alert {
|
||||
function warn($warning, $id) {
|
||||
print '<div class="warning">' . $warning . '</div>';
|
||||
print '<style type="text/css">#' . $id . ' { background-color: red }</style>';
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
</p>
|
||||
<p>
|
||||
Portons notre attention à la méthode <span class="new_code">makePayment()</span>.
|
||||
Si nous n'inscrivons pas un numéro "CVV2" (celui à trois
|
||||
chiffre au dos de la carte bleue), nous souhaitons afficher
|
||||
une erreur plutôt que d'essayer de traiter le paiement.
|
||||
En mode test...
|
||||
<pre>
|
||||
<?php
|
||||
require_once('simpletest/autorun.php');
|
||||
require_once('payment_form.php');
|
||||
Mock::generate('Alert');
|
||||
Mock::generate('PaymentGateway');
|
||||
|
||||
class PaymentFormFailuresShouldBeGraceful extends UnitTestCase {
|
||||
|
||||
function testMissingCvv2CausesAlert() {
|
||||
$alert = new MockAlert();
|
||||
<strong>$alert->expectOnce(
|
||||
'warn',
|
||||
array('Missing three digit security code', 'cvv2'));</strong>
|
||||
$controller = new PaymentForm(<strong>$alert</strong>, new MockPaymentGateway());
|
||||
$controller->makePayment($this->requestWithMissingCvv2());
|
||||
}
|
||||
|
||||
function requestWithMissingCvv2() { ... }
|
||||
}
|
||||
?>
|
||||
</pre>
|
||||
Première question : où sont passés les assertions ?
|
||||
</p>
|
||||
<p>
|
||||
L'appel à <span class="new_code">expectOnce('warn', array(...))</span> annonce
|
||||
à l'objet fantaisie qu'il faut s'attendre à un appel à <span class="new_code">warn()</span>
|
||||
avant la fin du test.
|
||||
Quand il débouche sur l'appel à <span class="new_code">warn()</span>, il vérifie
|
||||
les arguments. Si ceux-ci ne correspondent pas, alors un échec
|
||||
est généré. Il échouera aussi si la méthode n'est jamais appelée.
|
||||
</p>
|
||||
<p>
|
||||
Non seulement le test ci-dessus s'assure que <span class="new_code">warn</span>
|
||||
a bien été appelé, mais en plus qu'il a bien reçu la chaîne
|
||||
de caractère "Missing three digit security code"
|
||||
et même le tag "cvv2".
|
||||
L'équivalent de <span class="new_code">assertIdentical()</span> est appliqué
|
||||
aux deux champs quand les paramètres sont comparés.
|
||||
</p>
|
||||
<p>
|
||||
Si le contenu du message vous importe peu, surtout dans le cas
|
||||
d'une interface utilisateur qui change régulièrement,
|
||||
nous pouvons passer ce paramètre avec l'opérateur "*"...
|
||||
<pre>
|
||||
class PaymentFormFailuresShouldBeGraceful extends UnitTestCase {
|
||||
|
||||
function testMissingCvv2CausesAlert() {
|
||||
$alert = new MockAlert();
|
||||
$alert->expectOnce('warn', array(<strong>'*'</strong>, 'cvv2'));
|
||||
$controller = new PaymentForm($alert, new MockPaymentGateway());
|
||||
$controller->makePayment($this->requestWithMissingCvv2());
|
||||
}
|
||||
|
||||
function requestWithMissingCvv2() { ... }
|
||||
}
|
||||
</pre>
|
||||
Nous pouvons même rendre le test encore moins spécifique
|
||||
en supprimant complètement la liste des paramètres...
|
||||
<pre>
|
||||
function testMissingCvv2CausesAlert() {
|
||||
$alert = new MockAlert();
|
||||
<strong>$alert->expectOnce('warn');</strong>
|
||||
$controller = new PaymentForm($alert, new MockPaymentGateway());
|
||||
$controller->makePayment($this->requestWithMissingCvv2());
|
||||
}
|
||||
</pre>
|
||||
Ceci vérifiera uniquement si la méthode a été appelé,
|
||||
ce qui est peut-être un peu drastique dans ce cas.
|
||||
Plus tard, nous verrons comment alléger les attentes
|
||||
plus précisement.
|
||||
</p>
|
||||
<p>
|
||||
Des tests sans assertions peuvent être à la fois compacts
|
||||
et très expressifs. Parce que nous interceptons l'appel
|
||||
sur le chemin de l'objet, ici de classe <span class="new_code">Alert</span>,
|
||||
nous évitons de tester l'état par la suite.
|
||||
Cela évite les assertions dans les tests, mais aussi
|
||||
l'obligation d'ajouter des accesseurs uniquement
|
||||
pour les tests dans le code original.
|
||||
Si vous en arrivez à ajouter des accesseurs de ce type,
|
||||
on parle alors de "state based testing" dans le jargon
|
||||
("test piloté par l'état"),
|
||||
il est probablement plus que temps d'utiliser
|
||||
des objets fantaisie dans vos tests.
|
||||
On peut alors parler de "behaviour based testing"
|
||||
(ou "test piloté par le comportement") :
|
||||
c'est largement mieux !
|
||||
</p>
|
||||
<p>
|
||||
Ajoutons un autre test.
|
||||
Assurons nous que nous essayons même pas un paiement sans CVV2...
|
||||
<pre>
|
||||
class PaymentFormFailuresShouldBeGraceful extends UnitTestCase {
|
||||
|
||||
function testMissingCvv2CausesAlert() { ... }
|
||||
|
||||
function testNoPaymentAttemptedWithMissingCvv2() {
|
||||
$payment_gateway = new MockPaymentGateway();
|
||||
<strong>$payment_gateway->expectNever('pay');</strong>
|
||||
$controller = new PaymentForm(new MockAlert(), $payment_gateway);
|
||||
$controller->makePayment($this->requestWithMissingCvv2());
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
</pre>
|
||||
Vérifier une négation peut être très difficile
|
||||
dans les tests, mais <span class="new_code">expectNever()</span>
|
||||
rend l'opération très facile heureusement.
|
||||
</p>
|
||||
<p>
|
||||
<span class="new_code">expectOnce()</span> et <span class="new_code">expectNever()</span> sont
|
||||
suffisants pour la plupart des tests, mais
|
||||
occasionnellement vous voulez tester plusieurs évènements.
|
||||
D'ordinaire pour des raisons d'usabilité, nous souhaitons
|
||||
que tous les champs manquants du formulaire soient
|
||||
mis en relief, et pas uniquement le premier.
|
||||
Cela veut dire que nous devrions voir de multiples appels
|
||||
à <span class="new_code">Alert::warn()</span>, pas juste un...
|
||||
<pre>
|
||||
function testAllRequiredFieldsHighlightedOnEmptyRequest() {
|
||||
$alert = new MockAlert();<strong>
|
||||
$alert->expectAt(0, 'warn', array('*', 'cc_number'));
|
||||
$alert->expectAt(1, 'warn', array('*', 'expiry'));
|
||||
$alert->expectAt(2, 'warn', array('*', 'cvv2'));
|
||||
$alert->expectAt(3, 'warn', array('*', 'card_holder'));
|
||||
$alert->expectAt(4, 'warn', array('*', 'address'));
|
||||
$alert->expectAt(5, 'warn', array('*', 'postcode'));
|
||||
$alert->expectAt(6, 'warn', array('*', 'country'));
|
||||
$alert->expectCallCount('warn', 7);</strong>
|
||||
$controller = new PaymentForm($alert, new MockPaymentGateway());
|
||||
$controller->makePayment($this->requestWithMissingCvv2());
|
||||
}
|
||||
</pre>
|
||||
Le compteur dans <span class="new_code">expectAt()</span> précise
|
||||
le nombre de fois que la méthode a déjà été appelée.
|
||||
Ici nous vérifions que chaque champ sera bien mis en relief.
|
||||
</p>
|
||||
<p>
|
||||
Notez que nous sommes forcé de tester l'ordre en même temps.
|
||||
SimpleTest n'a pas encore de moyen pour éviter cela,
|
||||
mais dans une version future ce sera corrigé.
|
||||
</p>
|
||||
<p>
|
||||
Voici la liste complètes des attentes
|
||||
que vous pouvez préciser sur une objet fantaisie
|
||||
dans <a href="http://simpletest.org/">SimpleTest</a>.
|
||||
Comme pour les assertions, ces méthodes prennent en option
|
||||
un message d'erreur.
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>Attente</th>
|
||||
<th>Description</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="new_code">expect($method, $args)</span></td>
|
||||
<td>Les arguements doivent correspondre si appelés</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="new_code">expectAt($timing, $method, $args)</span></td>
|
||||
<td>Les arguements doiven correspondre si appelés lors du passage numéro <span class="new_code">$timing</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="new_code">expectCallCount($method, $count)</span></td>
|
||||
<td>La méthode doit être appelée exactement <span class="new_code">$count</span> fois</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="new_code">expectMaximumCallCount($method, $count)</span></td>
|
||||
<td>La méthode ne doit pas être appelée plus de <span class="new_code">$count</span> fois</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="new_code">expectMinimumCallCount($method, $count)</span></td>
|
||||
<td>La méthode ne doit pas être appelée moins de <span class="new_code">$count</span> fois</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="new_code">expectNever($method)</span></td>
|
||||
<td>La méthode ne doit jamais être appelée</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="new_code">expectOnce($method, $args)</span></td>
|
||||
<td>La méthode ne doit être appelée qu'une seule fois et avec les arguments (en option)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="new_code">expectAtLeastOnce($method, $args)</span></td>
|
||||
<td>La méthode doit être appelée au moins une seule fois et toujours avec au moins un des arguments attendus</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
Où les paramètres sont...
|
||||
<dl>
|
||||
<dt class="new_code">$method</dt>
|
||||
<dd>
|
||||
Le nom de la méthode, sous la forme d'une chaîne de caractères,
|
||||
à laquelle il faut appliquer la condition.
|
||||
</dd>
|
||||
<dt class="new_code">$args</dt>
|
||||
<dd>
|
||||
Les argumetns sous la forme d'une liste.
|
||||
Les jokers peuvent être inclus de la même manière
|
||||
que pour <span class="new_code">setReturn()</span>.
|
||||
Cet argument est optionnel pour <span class="new_code">expectOnce()</span>
|
||||
et <span class="new_code">expectAtLeastOnce()</span>.
|
||||
</dd>
|
||||
<dt class="new_code">$timing</dt>
|
||||
<dd>
|
||||
La seule marque dans le temps pour tester la condition.
|
||||
Le premier appel commence à zéro et le comptage se fait
|
||||
séparement sur chaque méthode.
|
||||
</dd>
|
||||
<dt class="new_code">$count</dt>
|
||||
<dd>Le nombre d'appels attendu.</dd>
|
||||
</dl>
|
||||
</p>
|
||||
<p>
|
||||
Si vous n'avez qu'un seul appel dans votre test, assurez vous
|
||||
d'utiliser <span class="new_code">expectOnce</span>.<br>
|
||||
Utiliser <span class="new_code">$mocked->expectAt(0, 'method', 'args);</span>
|
||||
tout seul ne permettra qu'à la méthode de ne jamais être appelée.
|
||||
Vérifier les arguements et le comptage total sont pour le moment
|
||||
indépendants.
|
||||
Ajouter une attente <span class="new_code">expectCallCount()</span> quand
|
||||
vous utilisez <span class="new_code">expectAt()</span> (dans le cas sans appel)
|
||||
est permis.
|
||||
</p>
|
||||
<p>
|
||||
Comme les assertions à l'intérieur des scénarios de test,
|
||||
toutes ces attentes peuvent incorporer une surchage
|
||||
sur le message sous la forme d'un paramètre supplémentaire.
|
||||
Par ailleurs le message original peut être inclus dans la sortie
|
||||
avec "%s".
|
||||
</p>
|
||||
|
||||
</div>
|
||||
References and related information...
|
||||
<ul>
|
||||
<li>
|
||||
Le papier original sur les Objets fantaisie ou
|
||||
<a href="http://www.mockobjects.com/">Mock objects</a>.
|
||||
</li>
|
||||
<li>
|
||||
La page du projet SimpleTest sur <a href="http://sourceforge.net/projects/simpletest/">SourceForge</a>.
|
||||
</li>
|
||||
<li>
|
||||
La page d'accueil de SimpleTest sur <a href="http://www.lastcraft.com/simple_test.php">LastCraft</a>.
|
||||
</li>
|
||||
</ul>
|
||||
<div class="menu_back"><div class="menu">
|
||||
<a href="index.html">SimpleTest</a>
|
||||
|
|
||||
<a href="overview.html">Overview</a>
|
||||
|
|
||||
<a href="unit_test_documentation.html">Unit tester</a>
|
||||
|
|
||||
<a href="group_test_documentation.html">Group tests</a>
|
||||
|
|
||||
<a href="mock_objects_documentation.html">Mock objects</a>
|
||||
|
|
||||
<a href="partial_mocks_documentation.html">Partial mocks</a>
|
||||
|
|
||||
<a href="reporter_documentation.html">Reporting</a>
|
||||
|
|
||||
<a href="expectation_documentation.html">Expectations</a>
|
||||
|
|
||||
<a href="web_tester_documentation.html">Web tester</a>
|
||||
|
|
||||
<a href="form_testing_documentation.html">Testing forms</a>
|
||||
|
|
||||
<a href="authentication_documentation.html">Authentication</a>
|
||||
|
|
||||
<a href="browser_documentation.html">Scriptable browser</a>
|
||||
</div></div>
|
||||
<div class="copyright">
|
||||
Copyright<br>Marcus Baker 2006
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user