Tests unitaires en PHP #1 prendre en main PHPUnit

Découvrir PHPUnit en écrivant 4 premiers tests.

→ Challenge Correction: Pierre-Feuille-Ciseaux

Tests unitaires et PHPUnit ?

Les tests unitaires sont une pierre angulaire du développement web. Ils permettent d’assurer que chaque « unité » de code fonctionne comme prévu et que des modifications réalisées dans le code n’introduisent pas de bug inattendus. Si tu n’es pas familier du concept, je ne peux que te conseiller notre article : introduction aux tests unitaires, sans frameworks. Si c’est OK, alors continue 🙂

En PHP, l’un des outils les plus reconnus pour cette tâche est PHPUnit. C’est un framework de tests qui facilite la création et l’exécution des tests pour ton code PHP. Dans cet article, nous allons le découvrir en écrivant de premiers tests. PHPUnit deviendra alors un allié inestimable dans ta quête d’un code robuste et fiable !

Au programme :

Organisation du code

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

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

On crée un premier objet en PHP

Dans le dossier /challenges/PIERRE_FEUILLE_CISEAUX je crée un objet 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 unitaires 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 !

On finalise notre objet pour que les tests passent au vert

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

On a vu dans un précédent article qu’on peut résoudre ce challenge en le codant de 11 façons différentes. 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 grâce aux tests

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.

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 ?

Keep learning

Other content to discover