Le fournisseur de données doit donc retourner un tableau ou un intégrateur contenant les arguments qui seront automatiquement transmis par atoum à toutes les méthodes de test auxquelles est rattaché le fournisseur de données lors de l’exécution des tests.

<?php

class calculator extends atoum
{
    /**
     * @dataProvider sumDataProvider
     */
    public function testSum($a, $b)
    {
        $this
            ->if($calculator = new project\calculator())
            ->then
                ->integer($calculator->sum($a, $b))->isEqualTo($a + $b)
        ;
    }
 
	 public function sumDataProvider()
    {
        return array(
            array( 1, 1),
            array( 1, 2),
            array(-1, 1),
            array(-1, 2),
        );
    }
}

Et si j’ai toujours trouvé le concept intéressant dans le fond, j’ai toujours trouvé que la forme nuisait à la lisibilité des tests.

De plus, j’ai rarement rencontré un cas de figure ou leur utilisation était réellement pertinente.

L’un dans l’autre, cela explique pourquoi je n’ai jamais mis en œuvre les fournisseurs de données jusqu’à aujourd’hui alors que j’en ai réalisé l’implémentation.

Cependant, aujourd’hui, j’ai rencontré un cas de figure dans lequel leur utilisation s’imposait.

J’avais en effet défini deux méthodes de tests très similaires puisque la seule différence de comportement entre les deux méthodes que je testais était le fait que l’une devait retourner $this, tandis que la seconde devait renvoyer un clone de $this.

De plus, les deux méthodes étaient testées avec les mêmes arguments.

<?php

class path extends atoum
{
	…
	public function testRelativizeFrom()
	{
		$this
			->if($path = new testedClass('/a/b', '/'))
			->then
				->object($path->relativizeFrom($path))
					->isIdenticalTo($path)
					->toString->isEqualTo('.')
			->if($path = new testedClass('/a/b', '/'))
			->then
				->object($path->relativizeFrom(new testedClass('/a', '/')))
					->isIdenticalTo($path)
					->toString->isEqualTo('./b')
			->if($path = new testedClass('/a/b', '/'))
			->then
				->object($path->relativizeFrom(new testedClass('/a/', '/')))
					->isIdenticalTo($path)
					->toString->isEqualTo('./b')
			->if($path = new testedClass('/a/b', '/'))
			->then
				->object($path->relativizeFrom(new testedClass('/c', '/')))
					->isIdenticalTo($path)
					->toString->isEqualTo('../a/b')
			->if($path = new testedClass('/a/b', '/'))
			->then
				->object($path->relativizeFrom(new testedClass('/c/', '/')))
					->isIdenticalTo($path)
					->toString->isEqualTo('../a/b')
			->if($path = new testedClass('/a/b', '/'))
			->then
				->object($path->relativizeFrom(new testedClass('/c/d', '/')))
					->isIdenticalTo($path)
					->toString->isEqualTo('../../a/b')
			->if($path = new testedClass('/a/b', '/'))
			->then
				->object($path->relativizeFrom(new testedClass('/c/d/', '/')))
					->isIdenticalTo($path)
					->toString->isEqualTo('../../a/b')
			->if($path = new testedClass('/a/b', '/'))
			->then
				->object($path->relativizeFrom(new testedClass('/', '/')))
					->isIdenticalTo($path)
					->toString->isEqualTo('./a/b')
		;
	}

	public function testGetRelativizedPathFrom()
	{
		$this
			->if($path = new testedClass('/a/b', '/'))
			->then
				->object($path->getRelativizedPathFrom($path))
					->isNotIdenticalTo($path)
					->toString->isEqualTo('.')
			->if($path = new testedClass('/a/b', '/'))
			->then
				->object($path->getRelativizedPathFrom(new testedClass('/a', '/')))
					->isNotIdenticalTo($path)
					->toString->isEqualTo('./b')
			->if($path = new testedClass('/a/b', '/'))
			->then
				->object($path->getRelativizedPathFrom(new testedClass('/a/', '/')))
					->isNotIdenticalTo($path)
					->toString->isEqualTo('./b')
			->if($path = new testedClass('/a/b', '/'))
			->then
				->object($path->getRelativizedPathFrom(new testedClass('/c', '/')))
					->isNotIdenticalTo($path)
					->toString->isEqualTo('../a/b')
			->if($path = new testedClass('/a/b', '/'))
			->then
				->object($path->getRelativizedPathFrom(new testedClass('/c/', '/')))
					->isNotIdenticalTo($path)
					->toString->isEqualTo('../a/b')
			->if($path = new testedClass('/a/b', '/'))
			->then
				->object($path->getRelativizedPathFrom(new testedClass('/c/d', '/')))
					->isNotIdenticalTo($path)
					->toString->isEqualTo('../../a/b')
			->if($path = new testedClass('/a/b', '/'))
			->then
				->object($path->getRelativizedPathFrom(new testedClass('/c/d/', '/')))
					->isNotIdenticalTo($path)
					->toString->isEqualTo('../../a/b')
			->if($path = new testedClass('/a/b', '/'))
			->then
				->object($path->getRelativizedPathFrom(new testedClass('/', '/')))
					->isNotIdenticalTo($path)
					->toString->isEqualTo('./a/b')
		;
	}
	…
}

L’utilisation d’un fournisseur de données s’imposait donc afin de mutualiser la gestion des arguments afin de supprimer la duplication de code induite par le fait que j’utilisais dans les deux méthodes de test exactement les mêmes arguments.

De plus, grâce au fournisseur de données, je pourrais au besoin ajouter un cas de test supplémentaire pour chacune des deux méthodes de test en modifiant uniquement le fournisseur de données.

J’ai donc défini les annotations @dataProvider nécessaires pour chacune des deux méthodes de test, ainsi que la méthode nécessaire à la génération de leurs arguments.

Et je me suis alors rendu compte de la lourdeur du processus.

En effet, pour mettre en place mon fournisseur de données, j’ai été obligé d’ajouter l’annotation mentionnée précédemment pour chacune des deux méthodes afin de définir la méthode devant être utilisée comme telle.

De plus, j’ai été obligé d’ajouter à chacune de mes deux méthodes de test les arguments nécessaires.

Or, il est tout à fait possible de se passer totalement de la définition de l’annotation.

En effet, si une méthode de test accepte des arguments, elle fait nécessairement appel à un fournisseur de données.

Et si une convention existe permettant de déduire automatiquement à partir du nom d'une méthode de test le fournisseur de données correspondant, il devient alors possible pour le développeur des tests de ne pas annoter sa méthode de test pour indiquer à atoum le fournisseur de données qu’elle devra utiliser lors de l’exécution des tests.

La rédaction des tests deviendrait alors plus simple et intuitive…

J’ai donc modifié le code d’atoum afin que si une méthode de test accepte des arguments et qu’elle ne dispose pas de l’annotation @dataProvider, elle utilise alors automatiquement et sans aucune intervention de l’utilisateur le fournisseur de données modélisé par la méthode protégée ou privée portant le nom résultat de la concaténation du nom de la méthode de test et de la chaîne de caractères 'DataProvider'.

<?php

class path extends atoum
{
	…
	public function testRelativizeFrom($path, $directorySeparator, $fromPath, $fromDirectorySeparator, $relativePath)
	{
		$this
			->if($path = new testedClass($path, $directorySeparator))
			->then
				->object($path->relativizeFrom(new testedClass($fromPath, $fromDirectorySeparator)))
					->isIdenticalTo($path)
					->toString->isEqualTo($relativePath)
		;
	}

	public function testGetRelativizedPathFrom($path, $directorySeparator, $fromPath, $fromDirectorySeparator, $relativePath)
	{
		$this
			->if($path = new testedClass($path, $directorySeparator))
			->then
				->object($path->getRelativizedPathFrom(new testedClass($fromPath, $fromDirectorySeparator)))
					->isNotIdenticalTo($path)
					->toString->isEqualTo($relativePath)
		;
	}

	protected function testRelativizeFromDataProvider()
	{
		return array(
			array('/a/b', '/', '/a/b', '/', '.'),
			array('/a/b', '/', '/a', '/', './b'),
			array('/a/b', '/', '/a/', '/', './b'),
			array('/a/b', '/', '/c', '/', '../a/b'),
			array('/a/b', '/', '/c/', '/', '../a/b'),
			array('/a/b', '/', '/c/d', '/', '../../a/b'),
			array('/a/b', '/', '/c/d/', '/', '../../a/b'),
			array('/a/b', '/', '/', '/', './a/b')
		);
	}

	protected function testGetRelativizedPathFromDataProvider()
	{
		return $this->testRelativizeFromDataProvider();
	}
	…
}

Quelques lignes de code plus tard et quelques itérations successives puisque je n’ai pas compris immédiatement que l’annotation pouvait devenir totalement facultative, atoum facilitait donc encore un peu plus la vie du développeur des tests en lui permettant de ne pas avoir à se soucier de certains détails d’implémentation durant leur rédaction.

J’ai donc eu la preuve que suivre l’adage Eat your own dog food est le meilleur moyen d’améliorer son code pour un développeur !