PHP

Tests unitaires en PHP #1 prendre en main PHPUnit

Découvrir PHPUnit en écrivant 4 premiers tests.

→ Corrigé du Challenge : Pierre-Feuille-Ciseaux

Ce corrigé s’appuie sur l’organisation du code proposé dans la Sandbox PHP. De cette manière, les dossiers, fichiers et namespaces nécessaires sont prêts à être utilisés.

Je travaille ici à 2 endroits :

  • Dans le dossier /challenges/PIERRE_FEUILLE_CISEAUX où je vais créer une classe puis l’utiliser dans le fichier pierre_feuille_ciseaux_api.php
  • Dans le dossier /phpunit/PIERRE_FEUILLE_CISEAUX, dans le fichier Pierre_feuille_ciseauxTest.php

Organisation du code

Dans ce challenge, il est question de trouver le coup correspondant à celui de l’adversaire, puis de retourner la liste complète des coups à jouer pour battre l’adversaire à chaque coup. Je décide donc de créer 2 méthodes :

  • La première retournera le coup à jouer, en fonction d’un coup donné
  • La seconde, qui fera appel à la première, retournera l’ensemble des coups à jouer

Dans le dossier /challenges/PIERRE_FEUILLE_CISEAUX je crée une classe PFCGame dans un fichier PFCGame.php qui contiendra ces méthodes :


namespace Challenges\PIERRE_FEUILLE_CISEAUX;

final class PFCGame
{
	public function play(string $play): string
	{
		return '';
	}

	public function party(string $plays): string
	{
		return '';
	}
}

La structure de mes méthodes est prête.

  • La première s’appelle « play », prend en paramètre une chaine de caractères « play » et renverra une chaine de caractères.
  • La seconde s’appelle « party », prend en paramètre une chaine de caractères « plays » et renverra une chaine de caractères.
  • Les return ‘ ‘; sont temporaires et permettent de respecter le typage de sortie de chaque méthode.

Les premiers tests avec PHPUnit

Je peux écrire mes tests dès maintenant, pas besoin de coder les méthodes play et party tout de suite.

Dans le fichier Pierre_feuille_ciseauxTest.php, je commence par faire 2 choses :

  • Appeler la classe PFCGame
  • La charger dans la méthode setUp, pour l’avoir disponible dans toutes mes autres méthodes
use PHPUnit\Framework\TestCase;
use Challenges\PIERRE_FEUILLE_CISEAUX\PFCGame;

final class Pierre_feuille_ciseauxTest extends TestCase
{
	public function setUp(): void
	{
		parent::setUp();
		$this->PFCGame = new PFCGame;
	}
}

Pour tester le bon fonctionnement de ma méthode play, je dois vérifier 3 choses :

  • Si le play est « F » (Feuille), la méthode doit retourner « C » (Ciseaux)
  • Si le play est « C », la méthode doit retourner « P » (Pierre)
  • Si le play est « P », la méthode doit retourner « F »

Je peux donc écrire ces 3 tests :

public function test_Feuille_donne_Ciseaux(): void
{
	$this->assertEquals(
		$this->PFCGame->play('F'),
		'C'
	);
}

public function test_Ciseaux_donne_Pierre(): void
{
	$this->assertEquals(
		$this->PFCGame->play('C'),
		'P'
	);
}

public function test_Pierre_donne_Feuille(): void
{
	$this->assertEquals(
		$this->PFCGame->play('P'),
		'F'
	);
}

Et voilà ! 3 premiers tests écrits avec PHPUnit !

Dans chaque test, j’utilise la méthode de PHPUnit « assertEquals » qui pourrait se traduire en « vérifie que c’est égal ». Je vérifie donc que le retour de ma fonction play quand le paramètre est « F » est égal à « C » (pour le premier test).

Maintenant, je lance mes tests avec ma console : (rappel de la commande, à la racine du projet : ./bin/vendor/phpunit –testsuite PHPUnit )

Tout est normal ici, pas d’inquiétude !

Forcément, chaque test est faux puisque la fonction play n’est pas encore codée !

On a vu dans cet article qu’il y a (au moins) 10 façons de coder cette fonction. En voici une :

final class PFCGame
{
	public function play(string $play): string
	{
		if ($play === 'P') {
			return 'F';
		}

		if ($play === 'F') {
			return 'C';
		}

		if ($play === 'C') {
			return 'P';
		}
	}

	public function party(string $plays): string
	{
		return '';
	}
}

Si je relance mes tests :

Bien joué 😉

Un peu de refactoring en utilisant des constantes

Comme mes tests verrouillent le bon fonctionnement de ma méthode, je peux en profiter pour faire un peu de refactoring, c’est-à-dire réécrire mon code. Ici, je vais remplacer mes chaînes de caractères par des constantes de classe. Cela me permet d’avoir quelque chose d’encore plus lisible et de sécuriser mon code. Si je tape « CC » au lieu de « C » par inadvertance, mon code ne renvoie pas d’erreur. Alors que si j’écris self::CISEAU (manque le X) alors mon code renverra une erreur.

final class PFCGame
{
	private const PIERRE = 'P';
	private const FEUILLE = 'F';
	private const CISEAUX = 'C';

	public function play(string $play): string
	{
		if ($play === self::PIERRE) {
			return self::FEUILLE;
		}

		if ($play === self::FEUILLE) {
			return self::CISEAUX;
		}

		if ($play === self::CISEAUX) {
			return self::PIERRE;
		}
	}

	public function party(string $plays): string
	{
		return '';
	}
}

Si on relance les tests, on a toujours du vert, donc tout est OK !

Libre à toi de continuer à refactoriser ce code 😉 je crois qu’on peut faire mieux que ces 3 if…

Et un dernier test avec PHPUnit

Il reste à écrire le test pour la méthode party. Celle-ci va donc devoir renvoyer plusieurs coups.

  • Si l’adversaire fait « PFC » alors la méthode doit renvoyer « FCP »

Voici comment on peut écrire ce test :

public function test_partie_3_coups(): void
{
	$this->assertEquals(
		$this->PFCGame->party('PFC'),
		'FCP'
	);
}

Je vérifie que le résultat de la méthode « party » lorsqu’elle prend en paramètre « PFC » est égal à « FCP ».

Si je relance mes tests :

Affichage complet

Quelques explications :

  • Les « . » représentent les assertions qui ont bien fonctionnées
  • Les « F » représentent un échec
  • On nous explique ici « Failed asserting that two strings are equal » => Pas possible de vérifier que 2 chaines de caractères sont égales. Il y a ‘ ‘ d’un côté et « FCP » de l’autre.
  • Un récapitulatif à la fin :
    • 4 tests (4 méthodes de tests)
    • 4 assertions (une méthode de PHPUnit assert… a été utilisée)
    • 1 échec

Voici donc une version possible pour la méthode party :

public function party(string $plays): string
{
	$response = '';
	$nbPlays = strlen($plays) - 1;

	for ($i = 0; $i <= $nbPlays; $i++) {
		$response .= $this->play($plays[$i]);
	}

	return $response;
}

Et si je relance mes tests :

Quel magnifique vert !

Et voilà ! 4 tests d’écrits et validés avec PHPUnit !

Aller + loin

Là on a testé le « good path », « le bon chemin », c’est-à-dire qu’on a testé que tout ce qui devait bien se passer se passe bien. C’est toujours par là qu’il faut commencer.

Pour aller + loin, on peut tester les cas limites, passer de mauvais paramètres, etc. c’est-à-dire le « wrong path », « le mauvais » chemin et donc vérifier que ce qui ne doit pas se passer ne se passe pas. Qu’est ce qui se passe si je passe une chaîne vide à la méthode play, si je passe un autre caractère que « P », « F » ou « C », etc.

On aurait pu aussi faire + de tests pour la méthode party, avec 1, 2, 3, 5, 10, 20 coups.

Enfin, tu l’as peut-être remarqué… mais la méthode play qui n’est utilisée que dans la classe devrait être « private » (quelques bonnes pratiques PHP présentées ici), il faudrait donc s’organiser pour pouvoir tester une méthode « private ».

Pour continuer dans ce sujet des tests unitaires, tu peux découvrir Les tests unitaires avec PHP #2.

Résoudre le challenge

On a bien codé ! Pour aller au bout, autant valider sa progression dans Tainix en se servant de ce code.

Dans le fichier pierre_feuille_ciseaux_api.php :

// CODE DU CHALLENGE ------------------
$output = (new PFCGame)->party($coups);


// REPONSE ----------------------------
$game->output(['data' => $output]);
Encore du vert !

Fichiers PHP finaux

Le code est également disponible sur Github.

PFCGame.php

declare(strict_types=1);

namespace Challenges\PIERRE_FEUILLE_CISEAUX;

final class PFCGame
{
	private const PIERRE = 'P';
	private const FEUILLE = 'F';
	private const CISEAUX = 'C';

	public function play(string $play): string
	{
		if ($play === self::PIERRE) {
			return self::FEUILLE;
		}

		if ($play === self::FEUILLE) {
			return self::CISEAUX;
		}

		if ($play === self::CISEAUX) {
			return self::PIERRE;
		}
	}

	public function party(string $plays): string
	{
		$response = '';
		$nbPlays = strlen($plays) - 1;

		for ($i = 0; $i <= $nbPlays; $i++) {
			$response .= $this->play($plays[$i]);
		}

		return $response;
	}
}

Pierre_feuille_ciseauxTest.php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;
use Challenges\PIERRE_FEUILLE_CISEAUX\PFCGame;

final class Pierre_feuille_ciseauxTest extends TestCase
{
	public function setUp(): void
	{
		parent::setUp();
		$this->PFCGame = new PFCGame;
	}

	public function test_Feuille_donne_Ciseaux(): void
	{
		$this->assertEquals(
			$this->PFCGame->play('F'),
			'C'
		);
	}

	public function test_Ciseaux_donne_Pierre(): void
	{
		$this->assertEquals(
			$this->PFCGame->play('C'),
			'P'
		);
	}

	public function test_Pierre_donne_Feuille(): void
	{
		$this->assertEquals(
			$this->PFCGame->play('P'),
			'F'
		);
	}

	public function test_partie_3_coups(): void
	{
		$this->assertEquals(
			$this->PFCGame->party('PFC'),
			'FCP'
		);
	}
}


Qui a codé ce superbe contenu ?


Ta newsletter chaque mois

Corrigés, challenges, actualités, veille technique... aucun spam.