Pour cela, j'ai utilisé le mécanisme d'interface proposé par PHP, car il se prête remarquablement bien à la mise en place de ce patron de conception.

Pour cela, j'ai commencé par définir l'interface des objets qui seront observés, de la manière suivante :

<?php

namespace mageekguy\atoum;

interface observable
{
public function callObservers($method);
}

?>

En effet, un objet observé doit pouvoir appeler ses observateurs pour leur demander l’exécution d'une méthode.

Dans un premier temps, j'avais également défini une méthode observable::addObserver(observer $observer), avant de réaliser que je n'avais aucune raison de donner cette responsabilité à mon interface.

En effet, l'ajout d'un observateur peut prendre bien des formes, et imposer un format spécifique pour cela peut vite devenir bloquant, comme nous allons le voir par la suite.

Une fois la définition de l'interface de mes objets observés, j'ai ensuite défini celle de mes observateurs, et elle s'est révélée très simple :

<?php

namespace mageekguy\atoum;

interface observer {}

?>

À première vue, cette interface ne sert strictement à rien, si ce n'est à définir le type observer.

Je l'ai pourtant conservé, car rien ne dit que je n'aurais pas à l'avenir des fonctionnalités à mutualiser à ce niveau.

J'ai ensuite fait la liste des événements pertinents susceptibles de survenir au cours de l'exécution des tests, et je les ai rattaché à un nom de méthode.

Au niveau de ma classe chargée de lancer les tests, nommée runner, je n'en ai identifié que deux :

  • Le début de l'exécution, rattaché à la méthode observer::runnerStart().
  • La fin de l'exécution, rattachée à la méthode observer::runnerStop().

Ensuite, jai créé à partir de cette liste l'interface dédié à l'observation d'un runner, qui a donc pris cette forme :

<?php

namespace mageekguy\atoum\observers;

use \mageekguy\atoum;

interface runner extends atoum\observer
{
public function runnerStart(atoum\runner $runner);
public function runnerStop(atoum\runner $runner);
}

?>

J'ai fais de même pour ma classe de test de base, nommée comme il se doit test.

J'ai ainsi obtenu la liste suivante :

  • Le début de l'exécution de la classe de test, rattaché à la méthode observer::testRunStart().
  • Le début de la mise en place de l'environnement de test, rattaché à la méthode observer::beforeTestSetup().
  • La fin de la mise en place de l'environnement de test, rattachée à la méthode observer::afterTestSetup().
  • Le début de l'exécution d'une méthode de test, rattaché à la méthode observer::beforeTestMethod().
  • Etc.

De la même façon que dans le cas du runner, j'ai obtenu l'interface suivante à partir de cette liste :

<?php

namespace mageekguy\atoum\observers;

use \mageekguy\atoum;

interface test extends atoum\observer
{
public function testRunStart(atoum\test $test);
public function beforeTestSetUp(atoum\test $test);
public function afterTestSetUp(atoum\test $test);
public function beforeTestMethod(atoum\test $test);
public function testAssertionFail(atoum\test $test);
public function testError(atoum\test $test);
public function testException(atoum\test $test);
public function testAssertionSuccess(atoum\test $test);
public function afterTestMethod(atoum\test $test);
public function beforeTestTearDown(atoum\test $test);
public function afterTestTearDown(atoum\test $test);
public function testRunStop(atoum\test $test);
}

?>

Une fois ces interfaces définies, il suffit ensuite de les implémenter au niveau de la classe chargée de la génération du rapport de test, nommée reporter.

<?php

namespace mageekguy\atoum;

class reporter implements observers\test
{
public function testRunStart(test $test) { ... return $this; }
public function beforeTestSetup(test $test) { ... return $this; }
public function afterTestSetup(test $test) { ... return $this; }
public function beforeTestMethod(test $test) { ... return $this; }
...

}

?>

Il faut ensuite d'implémenter l'interface observable créée précédemment et de définir une méthodes d'injection des observateurs au niveau de chacune des classes runner et test, par exemple de la façon suivante dans le cas de test :

<?php

namespace mageekguy\atoum;

use mageekguy\atoum;
use mageekguy\atoum\asserter;

abstract class test implements observable
{
protected $observers = array();
...
public function addObserver(atoum\observers\test $observer)
{
$this->observers[] = $observer;
return $this;
}

public function callObservers($method)
{
foreach ($this->observers as $observer)
{
$observer->{$method}($this);
}

return $this;
}
...

}

?>

Il n'y a plus ensuite qu'à faire appel à la méthode observable::callObservers() aux endroits stratégiques dans le code des classes test et runner pour pouvoir générer n'importe quel type de rapport, en fonction des observateurs qui sont rattachés aux instances de ces classes à l'aide des méthodes runner::addObserver() et test::addObserver().

Et pour créer ces observateurs, il suffit d'implémenter au niveau d'une classe l'interface correspondante, en fonction du résultat désiré.

De plus, cerise sur le gâteau, grâce à la puissance des interfaces, il est possible de définir une classe dont les instances peuvent simultanément être un observateur d'une instance de runner et d'une instance de test, puisqu'une classe peut implémenter plusieurs interfaces.

Dernière précision, si j'avais défini la méthode observable::addObserver(atoum\observer $observer) au niveau de l'interface observable comme j'avais envisagé de le faire dans un premier temps, je n'aurais pas pu définir le type précis d'observateur que les méthodes test::addObserver() et runner::addObserver() devaient accepter.

En effet, PHP refuse catégoriquement que la signature d'une méthode rendue obligatoire par une interface ne soit pas identique dans l'interface et dans les classes implémentant cette dernière.