Pour qu'un code soit testable, il faut pouvoir contrôler ses entrées/sorties, rien de plus, rien de moins.

Mais qu'est ce que cela veut dire ?

Dans le cadre de la programmation orientée objet, cela veut dire qu'il faut, pour chaque méthode publique de la classe à tester, connaître le résultat désiré en fonction des arguments de la méthode.

Le corrollaire coule de source, il faut pouvoir contrôler finement les arguments qui sont passés à la méthode lors des tests qui la concerne pour pouvoir valider la valeur de retour de la méthode.

Or, pour réaliser cela, il n'y a qu'une seule solution, et cela tombe bien, il s'agit d'une bonne pratique : Il faut découpler son code, c'est à dire limiter au maximum les dépendances entre les classes.

Mais concrétement, qu'est ce que cela veut dire ?

Tout simplement que le mot-clef new et tout autre méthode permettant d'instancier des objets, doivent être banni du code de vos méthodes.

En effet, vous ne pouvez pas contrôler les objets qui sont créés par vos méthodes.

Examinons le cas du code suivant :

class foo
{
protected $id = 0;

public function foo($id)
{
$this->id = $id;
}

public function load()
{
$dbClient = new dbClient();

return $dbClient->query('...');
}
}

Dans la méthode foo::load(), un objet du type dbClient est instancié.

Vous ne pouvez donc pas le contrôler puisque vous ne pouvez pas y accéder.

Vous ne pouvez pas définir des paramètres de connexion à la base de données invalides, vous ne pouvez pas simuler une impossibilité de connexion à la base, vous ne pouvez pas définir la base de données à utiliser, bref, votre périmétre de test est très limité, car vous ne pouvez tester que le cas le plus favorable, à la condition que les paramêtres par défaut du constructeur de la classe dbClient permettent de se connecter à votre base de données de test (je passerai sur l'ignominie qui consiste à utiliser $GLOBALS pour passer les paramêtres de connexion à dbClient...

Le code de test ressemble alors à cela :

class testFoo extends test
{
protected function testFoo()
{
$foo = new foo(rand(1, PHP_INT_MAX));
$this->isResource($foo->load());
}
}

La solution ? Découplez le code de la manière suivante :

class foo
{
protected $id = 0;

public function foo($id)
{
$this->id = $id;
}

public function load(dbClient $dbClient = null)
{
if ($dbClient === null)
{
$dbClient = new dbClient();
}

return $dbClient->query('...');
}
}

De cette manière, vous pouvez contrôler totalement l'instance de dbClient utilisée par votre code :

class testFoo extends test
{
...
protected function testFoo()
{
$dbClient = new dbClient(DB_HOST, DB_USER, DB_PASSWORD, DB_DATABASE);
$dbClient->disconnect();
$foo = new foo(rand(1, PHP_INT_MAX));
$this->false($dbClient->isConnected());
$this->error($foo->load($dbClient));
$dbClient->connect();
$this->true($dbClient->isConnected());

$this->isResource($foo->load($dbClient));
}
...
}

Evidement, le passage d'arguments et la gestion de la valeur par défaut vont être pénalisant en terme de performances, mais de cette façon, vous disposez d'un périmétre de test beaucoup plus large, d'autant que vous pouvez utiliser les mock pour simuler le comportement de votre instance de dbClient.

Cerise sur le gateau, vos tests sont plus complets, et donc documentent beaucoup plus finement votre code puisque vous pouvez tester tous les cas limites auxquels vous penserez.

Il serait donc dommage de se priver de cela pour économiser quelques ressources matérielles.