La solution est donc de s'en passer totalement, et de les remplacer par une coquille vide dont nous pourrons définir et tester le comportement lors de nos tests.

Cette coquille vide répond, dans le jargon des tests unitaires, au doux nom de mock.

Techniquement, un mock est une instance d'une classe qui reprend l'interface publique d'une classe, et dont il est possible de définir le comportement.

Il est ainsi possible de définir la valeur de retour de toutes les méthodes publiques de la classe en fonction de la valeur des arguments, et également de savoir les méthodes publiques qui ont été appelées avec les valeurs de leurs arguments.

Prenons par exemple le cas de l'un de mes projets personnels, pour lequel j'ai besoin de me connecter en tant que client à un serveur distant, via une connexion réseau, à l'aide d'une socket.

Or, le serveur n'existe pas, puisque je dois également le coder, et le protocole qui défini le dialogue entre le client et le serveur n'est même pas encore défini.

Je pourrais commencer par écrire le code du serveur, mais pour le tester, j'aurai besoin du client, et du protocole de communication, et pour pouvoir tester ce dernier, j'ai besoin du client et du serveur.

Bref, c'est l'histoire de la poule et de l'oeuf.

La solution est de mocker ma socket pour qu'elle simule la connexion au serveur à partir du client.

Pour ce faire, il faut commencer par écrire une classe qui permet d'appeler via un objet les fonctions de PHP relatives aux sockets.

En effet, un mock est basé sur l'interface publique d'une classe.

Et vu que PHP est un langage de merde qui n'est pas complétement objet, il faut transformer en objet tout ce qui ne l'est pas pour pouvoir utiliser les mocks, et une socket n'est pas un objet en PHP.

Le code de la classe en question doit être le plus simple possible, car c'est le seul code qui ne pourra jamais être testé, à moins de mettre en oeuvre un vrai serveur sur lequel il sera effectivement possible de se connecter, et en conséquence, il faut limiter au maximum le risque d'erreur.

class socketAdapter
{
public function __construct() {}

public function create($domain, $type, $protocol)
{ return socket_create($domain, $type, $protocol); }

public function close($resource)
{ return socket_close($resource); }

public function connect($resource, $address, $port = 0)
{ return socket_connect($resource, $address, $port); }

public function getErrorCode($resource = null)
{ return ($resource === null ? socket_last_error() : socket_last_error($resource)); }

public function getErrorString($errorCode)
{ return socket_strerror($errorCode); }
}

Une fois l'adaptateur défini, il faut créer la classe qui gérera effectivement la socket et qui utilisera cet adaptateur :

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

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

$this->adapter = $adapter;

$resource = $this->adapter->create(AF_INET, SOCK_STREAM, SOL_TCP);

if ($resource !== false) {
$this->resource = $resource;
} else {
throw $this->getException('Unable to create socket');
}
}

public function __destruct() {
$this->adapter->close($this->resource);
}

protected function getException($message) {
$errorCode = is_resource($this->resource) === false ? $this->adapter->getErrorCode() : $this->adapter->getErrorCode($this->resource);
return new phractalizerSocketException($message . ': ' . $this->adapter->getErrorString($errorCode), $errorCode);
}
}

class socketException extends exception {}

De cette manière, il devient possible de tester la classe socket en créant un mock de socketAdapter.

Un test resemble alors à cela :

class socketTest extends ogoUnitTest{
protected function test__construct() {
# Generation de la classe décrivant le mock à partir de l'interface publique de l'adaptateur
$socketApdaterClass = ogoMock::generate('socketAdapter', array(
'create',
'close',
'getErrorCode',
'getErrorString'
)
);

# Creation d'une instance du mock
$socketAdapter = new $socketApdaterClass();

$errorCode = rand(1, PHP_INT_MAX);
$errorString = uniqid();

# Définition des valeurs de retour des méthodes de l'adaptateur misent en oeuvre par notre code
$socketAdapter->getMock()
# create() doit renvoyer "false" indépendament de la valeur de ses arguments
->setReturn('create', false, array(ogoMock::wildcard, ogoMock::wildcard, ogoMock::wildcard))
# getErrorCode() doit renvoyer $errorCode indépendament de la valeur de ses arguments
->setReturn('getErrorCode', $errorCode, array(ogoMock::wildcard))
# Idem
->setReturn('getErrorString', $errorString, array($errorCode));

# Tests !
$socket = null;
$exception = null;
try { $socket = new socket($socketAdapter); } catch (exception $exception) {}
$this->null($socket);
$this->instance($exception, 'socketException');
$this->equal($exception->getCode(), $errorCode);
$this->equal($exception->getMessage(), 'Unable to create socket: ' . $errorString);

$socketAdapter->getMock()
# create() doit maintenant renvoyer quelquechose qui n'est pas "false" indépendament de la valeur de ses arguments
->setReturn('create', !false, array(ogoMock::wildcard, ogoMock::wildcard, ogoMock::wildcard));

$exception = null;
try { $socket = new socket($socketAdapter); } catch (exception $exception) {}
$this->instance($socket, 'socket');
$this->null($exception);
}

protected function test__destruct()
{
$socketApdaterClass = ogoMock::generate('socketAdapter', array
(
'create',
'close'
)
);

$socketAdapter = new $socketApdaterClass();
$socketAdapter->getMock()
->setReturn('create', !false, array(ogoMock::wildcard, ogoMock::wildcard, ogoMock::wildcard));

$socket = new socket($socketAdapter);
$this->equal($socketAdapter->getMock()->getMethodCall('close'), 0);
$socket->__destruct();
# Test pour savoir si la méthode close() de l'adaptateur a été effectivement appelé une fois
$this->equal($socketAdapter->getMock()->getMethodCall('close'), 1);
}
}

Je suis donc maintenant capable de tester ma classe socket sans créer de vrai socket.

Je n'ai donc plus besoin d'un serveur sur lequel me connecter, et je pourrais par la suite tester le protocole de communication en vérifiant que mon client envoit bien sur la socket les bonnes informations, et que mon serveur fait de même.