Prenons par exemple le cas d'une méthode qui utilise la fonction fopen() :

class foo
{
protected $resource = null;

function __construct() {}

function open($path)
{
$resource = fopen($path, 'w');

if ($resource === false)
{
throw new fooException('Unable to open file \'' . $path . '\'');
}
else
{
$this->resource = $resource;
return $this;
}
}
}

S'il est très facile de tester ce code dans le cas le plus favorable, puisqu'il suffit d'indiquer un chemin valide sur lequel l'utilsateur dispose des droits nécéssaires, les choses se compliquent pour tester le cas défavorable.

En effet, il faut arriver à créer le contexte qui ferait que fopen() ne renvoit pas une ressource.

Or, cela n'est pas évident.

fopen() serait une méthode et non une fonction, il suffirait de mocker l'objet correspondant, mais malheureusement, ce n'est pas le cas.

Qu'à cela ne tienne, il suffit d'encapsuler l'appel à fopen() dans une classe, pour pouvoir ainsi utiliser la technique du mock, comme je l'ai expliqué dans ce billet.

Le code devient alors ceci :

class phpAdapter
{
function fopen($path, $mode)
{
return fopen($path, $mode);
}
}

class foo
{
protected $adapter = null;
protected $resource = null;

function __construct(phpAdapter $adapter = null)
{
if ($adapter === null)
{
$adapter = new phpAdapter();
}

$this->adapter = $adapter;
}

function open($path)
{
$resource = $this->adapter->fopen($path, 'w');

if ($resource === false)
{
throw new fooException('Unable to open file \'' . $path . '\'');
}
else
{
$this->resource = $resource;
return $this;
}
}
}

Le code de test ressemble alors à cela :

class testFoo extends test
{
function testOpen()
{
$mockController = new mockController();
$mockGenerator = new mockGenerator();
$mockGenerator->generate('phpAdapter', 'phpAdapterMocked');

$phpAdapter = new phpAdapterMocked();
$phpAdapter->setMockController($mockController);

$foo = new foo($phpAdapter);
$mockController->setReturn('fopen', false);
$exception = null;
try { $foo->open(uniqid()); } catch (exception $exception) {}
$this->notNull($exception);
}
}

VIa la classe phpAdapter, il devient possible de contrôler le comportement de fopen().

Cependant, cette façon de faire présente un inconvénient.

Si vous utilisez des fonctions relatives aux dates, aux bases de données, aux fichiers, dans plusieurs de vos classes, vous êtes obligé de rajouter les fonctions correspondante dans la classe phpAdapter.

La classe phpAdapter va donc contenir de plus en plus de code en fonction de vos besoins en fonctions natives de PHP, alors même que certaines de vos classes n'en n'utiliseront que quelques méthodes.

Elle va donc devenir une classe monolithique, fourre-tout, et son utilisation deviendra donc pénalisante, à  la fois en terme de performance et en terme de maintenance.

La solution ? Faire une classe adapter par domaine fonctionnel, soit une classe timeAdapter pour la gestion du temps, une classe mysqlAdapter pour l'exploitation d'une base de données mysql, etc, ce qui évitera d'avoir une seule classe monolithique pour gérer l'ensemble des besoins.

Il suffit ensuite de passer en argument du constructeur de votre classes les objets de type adapter dont elle a besoin pour fonctionner et être testé.

Cependant, si votre classe utilise plusieurs adapter, il faut mocker chacun d'eux dans les tests, et le passage d'arguments au contructeur de la classe à tester devient vite laborieux.

De plus, si la technique du mock est puissante, elle est également très emmerdante lourde à mettre en oeuvre par le développeur.

Enfin, la classe qui définie l'adpater ne peut pas être testée, puisqu'il faudrait un adapter pour tester l'adapter.

Du coup, j'ai cherché une solution qui permette de n'avoir qu'un seul adapter testable et capable de gérer toutes les fonctions de PHP, et grâce à __call() et les classes de reflexion, j'ai trouvé la solution.

Le code précédent devient alors ceci :

class testFoo extends test
{
function testOpen()
{
$phpAdapter = new phpAdapter;
$foo = new foo($phpAdapter);
$phpAdapter->setReturn('fopen', false);
$exception = null;
try { $foo->open(uniqid()); } catch (exception $exception) {}
$this->notNull($exception);
}
}

Si dans le cas de cet exemple, qui ne met en oeuvre qu'un seul adapter, le gain n'est pas très probant, à partir de trois, il devient carrément phénoménal puisqu'il n'y a plus besoin de mocker chacun d'eux.

Si cette méthode intéresse quelqu'un, le code est disponible dans ogo, mon bac à sable personnel pour tout ce qui concerne PHP.

La documentation est quand à elle au format test unitaire.

Attention, PHP étant ce qu'il est, si la fonction PHP que vous souhaitez utiliser reçoit des arguments par référence, comme sort(), par exemple, il est nécéssaire d'utiliser la méthode invoke() de ma classe pour avoir un comportement correct.

Il y a un test dédié à ce comportement particulier dans les tests unitaires par sécurité.

Le code de la classe n'est pas encore figé, mais les évolutions à venir devraientt être relativement mineures.

Et bien évidement, ma classe peut être utilisée dans le cadre décrit par ce billet.

PS : le développement de cette classe a fait surgir le bug #48553...