XMLDB/simpletest/docs/fr/partial_mocks_documentation.html

461 lines
18 KiB
HTML
Raw Permalink Normal View History

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Documentation SimpleTest : les objets fantaisie partiels</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 partiels</h1>
This page...
<ul>
<li>
<a href="#injection">Le problème de l'injection d'un objet fantaisie</a>.
</li>
<li>
Déplacer la création vers une méthode <a href="#creation">fabrique protégée</a>.
</li>
<li>
<a href="#partiel">L'objet fantaisie partiel</a> génère une sous-classe.
</li>
<li>
Les objets fantaisie partiels <a href="#moins">testent moins qu'une classe</a>.
</li>
</ul>
<div class="content">
<p>
Un objet fantaisie partiel n'est ni plus ni moins
qu'un modèle de conception pour soulager un problème spécifique
du test avec des objets fantaisie, celui de placer
des objets fantaisie dans des coins serrés.
Il s'agit d'un outil assez limité et peut-être même
une idée pas si bonne que ça. Elle est incluse dans SimpleTest
pour la simple raison que je l'ai trouvée utile
à plus d'une occasion et qu'elle m'a épargnée
pas mal de travail dans ces moments-là.
</p>
<p><a class="target" name="injection"><h2>Le problème de l'injection dans un objet fantaisie</h2></a></p>
<p>
Quand un objet en utilise un autre il est très simple
d'y faire circuler une version fantaisie déjà prête
avec ses attentes. Les choses deviennent un peu plus délicates
si un objet en crée un autre et que le créateur est celui
que l'on souhaite tester. Cela revient à dire que l'objet
créé devrait être une fantaisie, mais nous pouvons
difficilement dire à notre classe sous test de créer
un objet fantaisie plutôt qu'un "vrai" objet.
La classe testée ne sait même pas qu'elle travaille dans un environnement de test.
</p>
<p>
Par exemple, supposons que nous sommes en train
de construire un client telnet et qu'il a besoin
de créer une socket réseau pour envoyer ses messages.
La méthode de connexion pourrait ressemble à quelque chose comme...
<pre>
<strong>&lt;?php
require_once('socket.php');
class Telnet {
...
function &amp;connect($ip, $port, $username, $password) {
$socket = &amp;new Socket($ip, $port);
$socket-&gt;read( ... );
...
}
}
?&gt;</strong>
</pre>
Nous voudrions vraiment avoir une version fantaisie
de l'objet socket, que pouvons nous faire ?
</p>
<p>
La première solution est de passer la socket en
tant que paramètre, ce qui force la création
au niveau inférieur. Charger le client de cette tâche
est effectivement une bonne approche si c'est possible
et devrait conduire à un remaniement -- de la création
à partir de l'action. En fait, c'est là une des manières
avec lesquels tester en s'appuyant sur des objets fantaisie
vous force à coder des solutions plus resserrées sur leur objectif.
Ils améliorent votre programmation.
</p>
<p>
Voici ce que ça devrait être...
<pre>
&lt;?php
require_once('socket.php');
class Telnet {
...
<strong>function &amp;connect(&amp;$socket, $username, $password) {
$socket-&gt;read( ... );
...
}</strong>
}
?&gt;
</pre>
Sous-entendu, votre code de test est typique d'un cas
de test avec un objet fantaisie.
<pre>
class TelnetTest extends UnitTestCase {
...
function testConnection() {<strong>
$socket = &amp;new MockSocket($this);
...
$telnet = &amp;new Telnet();
$telnet-&gt;connect($socket, 'Me', 'Secret');
...</strong>
}
}
</pre>
C'est assez évident que vous ne pouvez descendre que d'un niveau.
Vous ne voudriez pas que votre application de haut niveau
crée tous les fichiers de bas niveau, sockets et autres connexions
à la base de données dont elle aurait besoin.
Elle ne connaîtrait pas les paramètres du constructeur de toute façon.
</p>
<p>
La solution suivante est de passer l'objet créé sous la forme
d'un paramètre optionnel...
<pre>
&lt;?php
require_once('socket.php');
class Telnet {
...<strong>
function &amp;connect($ip, $port, $username, $password, $socket = false) {
if (!$socket) {
$socket = &amp;new Socket($ip, $port);
}
$socket-&gt;read( ... );</strong>
...
return $socket;
}
}
?&gt;
</pre>
Pour une solution rapide, c'est généralement suffisant.
Ensuite le test est très similaire : comme si le paramètre
était transmis formellement...
<pre>
class TelnetTest extends UnitTestCase {
...
function testConnection() {<strong>
$socket = &amp;new MockSocket($this);
...
$telnet = &amp;new Telnet();
$telnet-&gt;connect('127.0.0.1', 21, 'Me', 'Secret', &amp;$socket);
...</strong>
}
}
</pre>
Le problème de cette approche tient dans son manque de netteté.
Il y a du code de test dans la classe principale et aussi
des paramètres transmis dans le scénario de test
qui ne sont jamais utilisés. Il s'agit là d'une approche
rapide et sale, mais qui ne reste pas moins efficace
dans la plupart des situations.
</p>
<p>
Une autre solution encore est de laisser un objet fabrique
s'occuper de la création...
<pre>
&lt;?php
require_once('socket.php');
class Telnet {<strong>
function Telnet(&amp;$network) {
$this-&gt;_network = &amp;$network;
}</strong>
...
function &amp;connect($ip, $port, $username, $password) {<strong>
$socket = &amp;$this-&gt;_network-&gt;createSocket($ip, $port);
$socket-&gt;read( ... );</strong>
...
return $socket;
}
}
?&gt;
</pre>
Il s'agit là probablement de la réponse la plus travaillée
étant donné que la création est maintenant située
dans une petite classe spécialisée. La fabrique réseau
peut être testée séparément et utilisée en tant que fantaisie
quand nous testons la classe telnet...
<pre>
class TelnetTest extends UnitTestCase {
...
function testConnection() {<strong>
$socket = &amp;new MockSocket($this);
...
$network = &amp;new MockNetwork($this);
$network-&gt;setReturnReference('createSocket', $socket);
$telnet = &amp;new Telnet($network);
$telnet-&gt;connect('127.0.0.1', 21, 'Me', 'Secret');
...</strong>
}
}
</pre>
Le problème reste que nous ajoutons beaucoup de classes
à la bibliothèque. Et aussi que nous utilisons beaucoup
de fabriques ce qui rend notre code un peu moins intuitif.
La solution la plus flexible, mais aussi la plus complexe.
</p>
<p>
Peut-on trouver un juste milieu ?
</p>
<p><a class="target" name="creation"><h2>Méthode fabrique protégée</h2></a></p>
<p>
Il existe une technique pour palier à ce problème
sans créer de nouvelle classe dans l'application;
par contre elle induit la création d'une sous-classe au moment du test.
Premièrement nous déplaçons la création de la socket dans sa propre méthode...
<pre>
&lt;?php
require_once('socket.php');
class Telnet {
...
function &amp;connect($ip, $port, $username, $password) {<strong>
$socket = &amp;$this-&gt;_createSocket($ip, $port);</strong>
$socket-&gt;read( ... );
...
}<strong>
function &amp;_createSocket($ip, $port) {
return new Socket($ip, $port);
}</strong>
}
?&gt;
</pre>
Il s'agit là de la seule modification dans le code de l'application.
</p>
<p>
Pour le scénario de test, nous devons créer
une sous-classe de manière à intercepter la création de la socket...
<pre>
<strong>class TelnetTestVersion extends Telnet {
var $_mock;
function TelnetTestVersion(&amp;$mock) {
$this-&gt;_mock = &amp;$mock;
$this-&gt;Telnet();
}
function &amp;_createSocket() {
return $this-&gt;_mock;
}
}</strong>
</pre>
Ici j'ai déplacé la fantaisie dans le constructeur,
mais un setter aurait fonctionné tout aussi bien.
Notez bien que la fantaisie est placée dans une variable
d'objet avant que le constructeur ne soit attaché.
C'est nécessaire dans le cas où le constructeur appelle
<span class="new_code">connect()</span>.
Autrement il pourrait donner un valeur nulle à partir de
<span class="new_code">_createSocket()</span>.
</p>
<p>
Après la réalisation de tout ce travail supplémentaire
le scénario de test est assez simple.
Nous avons juste besoin de tester notre nouvelle classe à la place...
<pre>
class TelnetTest extends UnitTestCase {
...
function testConnection() {<strong>
$socket = &amp;new MockSocket($this);
...
$telnet = &amp;new TelnetTestVersion($socket);
$telnet-&gt;connect('127.0.0.1', 21, 'Me', 'Secret');
...</strong>
}
}
</pre>
Cette nouvelle classe est très simple bien sûr.
Elle ne fait qu'initier une valeur renvoyée, à la manière
d'une fantaisie. Ce serait pas mal non plus si elle pouvait
vérifier les paramètres entrants.
Exactement comme un objet fantaisie.
Il se pourrait bien que nous ayons à réaliser cette astuce régulièrement :
serait-il possible d'automatiser la création de cette sous-classe ?
</p>
<p><a class="target" name="partiel"><h2>Un objet fantaisie partiel</h2></a></p>
<p>
Bien sûr la réponse est "oui"
ou alors j'aurais arrêté d'écrire depuis quelques temps déjà !
Le test précédent a représenté beaucoup de travail,
mais nous pouvons générer la sous-classe en utilisant
une approche à celle des objets fantaisie.
</p>
<p>
Voici donc une version avec objet fantaisie partiel du test...
<pre>
<strong>Mock::generatePartial(
'Telnet',
'TelnetTestVersion',
array('_createSocket'));</strong>
class TelnetTest extends UnitTestCase {
...
function testConnection() {<strong>
$socket = &amp;new MockSocket($this);
...
$telnet = &amp;new TelnetTestVersion($this);
$telnet-&gt;setReturnReference('_createSocket', $socket);
$telnet-&gt;Telnet();
$telnet-&gt;connect('127.0.0.1', 21, 'Me', 'Secret');
...</strong>
}
}
</pre>
La fantaisie partielle est une sous-classe de l'original
dont on aurait "remplacé" les méthodes sélectionnées
avec des versions de test. L'appel à <span class="new_code">generatePartial()</span>
nécessite trois paramètres : la classe à sous classer,
le nom de la nouvelle classe et une liste des méthodes à simuler.
</p>
<p>
Instancier les objets qui en résultent est plutôt délicat.
L'unique paramètre du constructeur d'un objet fantaisie partiel
est la référence du testeur unitaire.
Comme avec les objets fantaisie classiques c'est nécessaire
pour l'envoi des résultats de test en réponse à la vérification des attentes.
</p>
<p>
Une nouvelle fois le constructeur original n'est pas lancé.
Indispensable dans le cas où le constructeur aurait besoin
des méthodes fantaisie : elles n'ont pas encore été initiées !
Nous initions les valeurs retournées à cet instant et
ensuite lançons le constructeur avec ses paramètres normaux.
Cette construction en trois étapes de "new",
suivie par la mise en place des méthodes et ensuite
par la lancement du constructeur proprement dit est
ce qui distingue le code d'un objet fantaisie partiel.
</p>
<p>
A part pour leur construction, toutes ces méthodes
fantaisie ont les mêmes fonctionnalités que dans
le cas des objets fantaisie et toutes les méthodes
non fantaisie se comportent comme avant.
Nous pouvons mettre en place des attentes très facilement...
<pre>
class TelnetTest extends UnitTestCase {
...
function testConnection() {
$socket = &amp;new MockSocket($this);
...
$telnet = &amp;new TelnetTestVersion($this);
$telnet-&gt;setReturnReference('_createSocket', $socket);<strong>
$telnet-&gt;expectOnce('_createSocket', array('127.0.0.1', 21));</strong>
$telnet-&gt;Telnet();
$telnet-&gt;connect('127.0.0.1', 21, 'Me', 'Secret');
...<strong>
$telnet-&gt;tally();</strong>
}
}
</pre>
</p>
<p><a class="target" name="moins"><h2>Tester moins qu'une classe</h2></a></p>
<p>
Les méthodes issues d'un objet fantaisie n'ont pas
besoin d'être des méthodes fabrique, Il peut s'agir
de n'importe quelle sorte de méthode.
Ainsi les objets fantaisie partiels nous permettent
de prendre le contrôle de n'importe quelle partie d'une classe,
le constructeur excepté. Nous pourrions même aller jusqu'à
créer des fantaisies sur toutes les méthodes à part celle
que nous voulons effectivement tester.
</p>
<p>
Cette situation est assez hypothétique, étant donné
que je ne l'ai jamais essayée. Je suis ouvert à cette possibilité,
mais je crains qu'en forçant la granularité d'un objet
on n'obtienne pas forcément un code de meilleur qualité.
Personnellement j'utilise les objets fantaisie partiels
comme moyen de passer outre la création ou alors
de temps en temps pour tester le modèle de conception TemplateMethod.
</p>
<p>
Pour choisir le mécanisme à utiliser, on en revient
toujours aux standards de code de votre projet.
</p>
</div>
References and related information...
<ul>
<li>
La page du projet SimpleTest sur
<a href="http://sourceforge.net/projects/simpletest/">SourceForge</a>.
</li>
<li>
<a href="http://simpletest.org/api/">L'API complète pour SimpleTest</a>
à partir de PHPDoc.
</li>
<li>
La méthode fabrique protégée est décrite dans
<a href="http://www-106.ibm.com/developerworks/java/library/j-mocktest.html">
cet article d'IBM</a>. Il s'agit de l'unique papier
formel que j'ai vu sur ce problème.
</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>