Utiliser les bouchons - tharkun/atoum GitHub Wiki
atoum dispose d'un système de bouchonnage puissant et simple à mettre en œuvre.
Pour créer un bouchon, il faut commencer par créer la classe correspondante à l'aide de la méthode mageekguy\atoum\test::mock()
:
<?php
namespace vendor\project\tests\units;
use
mageekguy\atoum,
vendor\project
;
require __DIR__ . '/mageekguy.atoum.phar';
class bankAccount extends atoum\test
{
public function testGetOperations()
{
$this->mock('vendor\project\database\client');
}
}
?>
Par défaut, la méthode mageekguy\atoum\test::mock()
crée une classe portant le même nom que la classe ou l'interface devant servir au bouchonnage, mais dans l'espace de nommage \mock
, et le code précédent a donc provoquer la définition de la classe \mock\vendor\project\database\client
.
Il est possible de modifier ce comportement en indiquant l'espace de nommage désiré pour la classe de bouchonnage via le second argument de mageekguy\atoum\test::mock()
:
<?php
namespace vendor\project\tests\units;
use
mageekguy\atoum,
vendor\project
;
require __DIR__ . '/mageekguy.atoum.phar';
class bankAccount extends atoum\test
{
public function testGetOperations()
{
$this->mock('vendor\project\database\client', '\database');
}
}
?>
Ainsi, le code précédent permet la création de la classe de bouchonnage \database\client
.
De plus, mageekguy\atoum\test::mock()
accepte également un troisième argument qui permet de définir le nom de la classe de bouchonnage.
Pour créer la classe \database\mockedClient
à partir de la classe \vendor\project\database\client
, il suffit donc d'utiliser le code suivant :
<?php
namespace vendor\project\tests\units;
use
mageekguy\atoum,
vendor\project
;
require __DIR__ . '/mageekguy.atoum.phar';
class bankAccount extends atoum\test
{
public function testGetOperations()
{
$this->mock('vendor\project\database\client', '\database', 'mockedClient');
}
}
?>
Il est à noter qu'une ancienne syntaxe est utilisée dans la plupart des tests unitaires de atoum, de la forme $this->mockGenerator->generate()
, mais il est recommandé d'utiliser mageekguy\atoum\test::mock()
afin de faciliter à la fois la rédaction des tests et leur relecture.
Une fois la classe de bouchonnage définie via mageekguy\atoum\test::mock()
, il n'y a plus qu'à en créer une instance très classiquement à l'aide de l'opérateur new
.
Afin de faciliter la rédaction des tests, il peut être pertinent de créer un alias de l'espace de nommage mock\vendor\project
à l'aide du mot-clef use
:
<?php
namespace vendor\project\tests\units;
use
mageekguy\atoum,
vendor\project,
mock\vendor\project as mock
;
require __DIR__ . '/mageekguy.atoum.phar';
class bankAccount extends atoum\test
{
public function testGetOperations()
{
$this->mock('vendor\project\database\client');
$databaseClient = new mock\database\client();
}
}
?>
Par défaut, une instance d'une classe bouchonnée se comporte exactement comme une instance de la classe à partir de laquelle le bouchon a été créé.
Dans notre cas, l'instance $databaseClient
de la classe \mock\vendor\project\database\client
se comporte donc pour l'instant tout comme une instance de la classe \vendor\project\database\client
.
Pour modifier son comportement, il faut passer par son contrôleur à l'aide de la méthode \mock\vendor\project\database\client::getMockController()
:
<?php
namespace vendor\project\tests\units;
use
mageekguy\atoum,
vendor\project,
mock\vendor\project as mock
;
require __DIR__ . '/mageekguy.atoum.phar';
class bankAccount extends atoum\test
{
public function testGetOperations()
{
$this->mock('vendor\project\database\client');
$databaseClient = new mock\database\client();
$databaseClient->getMockController()->connect = function() {};
$databaseClient->getMockController()->query = array();
}
}
?>
Le code précédent permet de définir le comportement des méthodes \mock\vendor\project\database\client::connect()
et \mock\vendor\project\database\client::query()
.
Le bouchon devant permettre d'exécuter les tests indépendamment de toute base de données, la méthode \mock\vendor\project\database\client::connect()
est surchargée avec une fonction anonyme qui ne fait rien.
Il aurait été également possible d'obtenir le même comportement à l'aide de la syntaxe $databaseClient->getMockController()->connect = null;
mais le fait d'utiliser une fonction anonyme explicitement rend le test plus lisible.
La méthode \mock\vendor\project\database\client::query()
est quand à elle surchargée via le contrôleur afin qu'elle renvoie systématiquement un tableau vide.
Évidemment, il est possible de définir un comportement plus complexe pour une méthode à l'aide d'une fonction anonyme ou d'une fermeture lexicale :
<?php
namespace vendor\project\tests\units;
use
mageekguy\atoum,
vendor\project,
mock\vendor\project as mock
;
require __DIR__ . '/mageekguy.atoum.phar';
class bankAccount extends atoum\test
{
public function testGetOperations()
{
$this->mock('vendor\project\database\client');
$databaseClient = new mock\database\client();
$databaseClient->getMockController()->connect = function() {};
$databaseClient->getMockController()->query = function($query) use (& $record) {
switch ($query)
{
case 'SELECT * FROM foo WHERE bar = 1':
return $records;
default:
return array();
}
};
}
}
?>
En résumé, il est possible de substituer implicitement ou explicitement au code de toutes les méthodes d'une classe de bouchonnage une fonction anonyme ou une fermeture lexicale.
Une fois le comportement du bouchon défini, il faut vérifier qu'il est utilisé correctement par la méthode testée, et pour cela, atoum dispose d'assertions dédiées :
<?php
namespace vendor\project\tests\units;
use
mageekguy\atoum,
vendor\project,
mock\vendor\project as mock
;
require __DIR__ . '/mageekguy.atoum.phar';
class bankAccount extends atoum\test
{
public function testGetOperations()
{
$this->mock('vendor\project\database\client');
$databaseClient = new mock\database\client();
$databaseClient->getMockController()->connect = function() {};
$databaseClient->getMockController()->query = array();
$bankAccount = new \vendor\project\bank\account();
$this->assert
->array($bankAccount->getOperation($databaseClient))->isEmpty()
->mock($databaseClient)
->call('query')->once()
;
}
}
?>
Le code précédent permet de vérifier que la méthode \mock\vendor\project\database\client::query()
est bien appelée une seule fois par la méthode \vendor\project\bank\account::getOperations()
grâce à l'assertion once()
.
Il est également possible de vérifier si une méthode n'a jamais été appelée, ou bien si elle a été appelée au moins une fois ou un nombre précis de fois, respectivement à l'aide des assertions never()
, atLeastOnce()
et exactly()
.
De plus, l'utilisateur a la possibilité de combiner ces assertions avec l'assertion withArguments()
afin de valider le fait qu'une méthode a bien été appelé avec les bons arguments :
<?php
namespace vendor\project\tests\units;
use
mageekguy\atoum,
vendor\project,
mock\vendor\project as mock
;
require __DIR__ . '/mageekguy.atoum.phar';
class bankAccount extends atoum\test
{
public function testGetOperations()
{
$this->mock('vendor\project\database\client');
$databaseClient = new mock\database\client();
$databaseClient->getMockController()->connect = function() {};
$databaseClient->getMockController()->query = array();
$bankAccount = new \vendor\project\bank\account();
$this->assert
->array($bankAccount->getOperation($databaseClient))->isEmpty()
->mock($databaseClient)
->call('query')->withArguments('SELECT * from operations')->once()
;
}
}
?>
Il peut parfois être nécessaire de bouchonner un constructeur, et pour réaliser cela, atoum propose deux solutions.
La première solution consiste à :
- Créer une instance de la classe
\mageekguy\atoum\mock\controller
avant d'appeler le constructeur du bouchon ; - Définir via ce contrôleur le comportement du constructeur du bouchon à l'aide d'une fonction anonyme ;
- Passer le contrôleur en dernier argument du constructeur du bouchon ;
De cette façon, la fonction anonyme surchargeant le constructeur sera exécutée au lieu du constructeur de la classe bouchonnée.
<?php
namespace vendor\project\tests\units;
use
mageekguy\atoum,
vendor\project,
mock\vendor\project as mock
;
require __DIR__ . '/mageekguy.atoum.phar';
class bankAccount extends atoum\test
{
public function testGetOperations()
{
$this->mock('vendor\project\database\client');
$controller = new atoum\mock\controller();
$controller->__construct = function() {};
$databaseClient = new mock\database\client($controller);
}
}
?>
La seconde solution est en grande partie similaire à la première, la seule différence étant à la dernière étape :
- Créer une instance de la classe
\mageekguy\atoum\mock\controller
avant d'appeler le constructeur du bouchon ; - Définir via ce contrôleur le comportement du constructeur du bouchon à l'aide d'une fonction anonyme ;
- Appeler sur le contrôleur la méthode
\mageekguy\atoum\mock\controller::injectInNextMockInstance()
.
Le contrôleur sera alors automatiquement injecté dans le prochain bouchon créé.
<?php
namespace vendor\project\tests\units;
use
mageekguy\atoum,
vendor\project,
mock\vendor\project as mock
;
require __DIR__ . '/mageekguy.atoum.phar';
class bankAccount extends atoum\test
{
public function testGetOperations()
{
$this->mock('vendor\project\database\client');
$controller = new atoum\mock\controller();
$controller->__construct = function() {};
$controller->injectInNextMockInstance();
$databaseClient = new mock\database\client();
}
}
?>
L'avantage de cette méthode est qu'une modification de la signature du constructeur d'un bouchon ne nécessitera pas une modification des tests, puisque le contrôleur n'est plus injecté dans l'instance du bouchon via un argument de son constructeur.
Cette documentation est en cours de rédaction, si vous souhaitez en apprendre plus au sujet des bouchons de atoum en attendant sa finalisation, nous vous invitons à étudier les tests unitaires de atoum.
Vous trouverez également de l'assistance sur atoum en général et sur le bouchonnage en particulier sur le canal IRC ##atoum sur le réseau freenode.