Créer un mock en PHP pour Atoum – partie 2

Créer un mock en PHP pour Atoum - partie 2

Voyons cette semaine un autre type de mock pour vos tests unitaires en PHP avec Atoum : le mock de classe, qui pourra être instancié par la classe à tester.

Ce type de mock m’a l’air assez évident, tellement évident qu’il n’est pas décrit sur la documentation. J’ai quand même dû chercher comment le mettre en œuvre. Ou alors je fais complétement fausse route.
En tout cas ça marche, et cette méthode colle bien à mon besoin.

La classe à tester

Le but de ce mock est de substituer une classe existante par une autre, pour limiter ses connexions avec l’extérieur (base de données, …) et contrôler ses retours.

Pour bien comprendre, voici un exemple de classe à tester :

namespace Vendor\Users;

class UsersManager {
	private $users = array();

	/**
	 * Add a user to the list
	 * @return User|boolean Return the created user object, or false if the user is not valid
	 */
	public function addUser($firstName, $lastName) {
		$user = new User($firstName, $lastName);
		if ($user->isValid()) {
			$this->users[] = $user;
			return $user;
		} else {
			return false;
		}
	}
}

Cette classe UsersManager gère une liste d’utilisateurs, représentés par des objets de type User :

class User {
	private $firstName;
	private $lastName;
	private $webService;

	public function __construct($firstName, $lastName) {
		$this->firstName = $firstName;
		$this->lastName = $lastName;
		$this->webService = new \Web\Service\WebService();
	}

	public function isValid() {
		// perform connection to an external service and return the result (true or false)
		return $webService->isValid($this->firstName . " " . $this->lastName);
	}

	public function unregister() {
		return $webService->unregister();
	}
}

Cette classe stocke le prénom et le nom de l’utilisateur et utilise un Web service pour savoir si l’utilisateur est valide.

Création d’une classe bouchon

Le principe est simple, un peu à l’ancienne en fait : on crée une classe qui ressemble fortement à celle utilisée par la classe à tester, notamment avec ses méthodes publiques.

Dans notre exemple, la méthode isValid de la classe User utilise un Web service, qu’il est judicieux de shunter dans le cadre d’un test unitaire. Ainsi, nous allons recréer cette méthode isValid.

Il n’est pas obligatoire de simuler toutes les méthodes publiques, seulement celle utilisées par la classe à tester.

Voici notre bouchon, nommé fakeUser, écrit directement dans le fichier de test de la classe UserManager :

namespace Vendor\Users\tests\units;

use atoum;

class fakeUser {
	private $firstName;
	private $lastName;

	public function __construct($firstName, $lastName) {
		$this->firstName = $firstName;
		$this->lastName = $lastName;
	}

	/**
	 * Return false when the first name begins with 'not-valid'
	 */
	public function isValid() {
		return (strpos($this->firstName, "not-valid") === 0);
	}
}

Le code du constructeur est très similaire à celui de la classe User d’origine, hormis l’instanciation du Web service qui a disparu.

La méthode unregister a aussi disparu, comme elle n’est pas utilisée par UserManager.

Enfin, la valeur renvoyée par la méthode isValid dépend du prénom de l’utilisateur : s’il commence par not-valid, false est renvoyé.

Il s’agit d’un subterfuge simple pour piloter plus finement le mock.

Définition du mock dans le test

Maintenant que notre classe bouchon est prête, nous pouvons la substituer à la classe d’origine.

Pour cela, il suffit d’utiliser la méthode generate du mockGenerator d’Atoum :

		$this->mockGenerator->generate('\Vendor\Users\tests\units\fakeUser', '\Vendor\Users', 'User');

Ici, le name space de la classe \Vendor\Users\tests\units\fakeUser est renommé en \Vendor\Users et son nom en User.

En clair, on renomme \Vendor\Users\tests\units\fakeUser en \Vendor\Users\User. Tout simplement.

Ce code peut être inséré au début de chaque méthode de test (test*) concernée ou dans la méthode beforeTestMethod exécutée automatiquement avant chaque méthode de test :

	public function beforeTestMethod() {
		$this->mockGenerator->generate('\Vendor\Users\tests\units\fakeUser', '\Vendor\Users', 'User');
	}

Utilisation du mock

Maintenant que tout est en place, notre classe UserManager peut être testée :

	public function testValidUser() {
		$this
			->given($m = new \Vendor\Users\UsersManager())
			->object($m->addUser("Maurice", "Moss"))
			// The class name is the real one, not the fake one, even it is a mock class:
			->isInstanceOf('\Vendor\Users\User');
	}

Comme la méthode addUser renvoie l’instance de l’objet User créé, un test est de vérifier que son retour est de type object et est une instance de \Vendor\Users\User.
En l’occurrence, c’est bien le cas, malgré qu’il s’agisse en réalité de notre bouchon.

C’est fort, non ?

Testons maintenant un utilisateur non valide :

	public function testInvalidUser() {
		$this
			->given($m = new \Vendor\Users\UsersManager())
			->boolean($m->addUser("not-valid-Maurice", "Moss"))
			->isEqualTo(false);
	}

En préfixant le prénom de notre utilisateur par not-valid, la méthode isValid de notre bouchon renvoie false.

Et nous avons testé en toute simplicité la méthode addUser de notre classe UserManager.

Le mot de la fin

Bien sûr, dans notre exemple, il reste à tester la véritable classe User, certainement en lui passant un objet mocké de Web service, comme nous l’avons vu dans l’article précédent.

Enfin, il existe une contrainte à ce genre de mock : il est nécessaire de le maintenir à chaque modification de la classe qu’il remplace.
En fait c’est un gage que les impacts apportés à la classe ont bien été mesurés et que la modification a bien été comprise.

Ce n’est peut-être pas une contrainte finalement…

L'illustration de cet article est une image sous licence CC BY-SA 3.0 par Frank Vincentz

Cet article vous a été utile ? Partage it !

4 réflexions au sujet de « Créer un mock en PHP pour Atoum – partie 2 »

    1. Bonjour Renaud,
      J’ai bien lu la documentation, et je suis preneur d’une solution plus dans l’esprit d’Atoum si elle existe.
      Dans l’exemple de cet article, comment tu instancies le bouchon dans la classe à tester, sans modifier la classe à tester ?
      David

  1. En fait, tu créés une dépendance forte entre UsersManager et User, ce qui n’est pas une bonne habitude.

    C’est à cause de ce problème que tu n’arrives pas facilement à tester UsersManager.
    Pour moi, la bonne signature de addUser devrait être addUser(UserInterface $user).
    Cela permet une plus grande souplesse sur le type d’User que tu veux ajouter, tout en garantissant, grâce à l’interface, qu’ils proposent bien une API précise que tu utiliseras dans ton application (voire uniquement dans l’UsersManager, par exemple avec une interface UserManageableInterface).

    Du coup, avec ton code actuel (que j’avais mal lu) et en fonction de la doc, l’astuce que tu proposes me semble être la seule et unique.

    En revanche, on peut voir dans le code du mockGenerator la méthode overload (https://github.com/atoum/atoum/blob/master/classes/mock/generator.php#L74-L79) qui permet de surcharger une méthode au niveau du mockGenerator (https://github.com/atoum/atoum/blob/master/tests/units/classes/mock/generator.php#L96-L104), pour que chaque mock généré utilise ce nouveau code.
    Mais c’est peut-être pas aussi simple à faire.

    D’une part, je te conseille d’ouvrir un ticket sur https://github.com/atoum/atoum/issues pour demander si 1. y’a un moyen simple et 2. si ca n’existe pas, de l’implémenter.
    D’autre part, tu peux également ouvrir un ticket sur https://github.com/atoum/atoum-documentation/issues pour demander à ce que la doc soit mise à jour.
    Pour l’un et pour l’autre, tu peux également participer toi-même en proposant une PR 🙂

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Si vous le souhaitez, renseignez le champ 'Nom' de cette façon : 'VotreNom@VotreMotClef' pour obtenir une ancre optimisée pour les moteurs de recherche.