T: / Corrigés des challenges / PHP
Découvrir PHPUnit en écrivant 6 nouveaux tests, dans un contexte objet.
Pour démarrer (si pas encore lu) : Tests unitaires en PHP #1
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 ce challenge, il est question d’analyser les résultats de plusieurs matchs et de définir le classement des équipes une fois tous les matchs joués.
Pour cela, je vais m’appuyer sur un jeu de données, pour lequel je connais le résultat. Ça tombe bien, chaque challenge propose un jeu de données, et le résultat qui va avec ! Il suffit de passer l’énoncé et de cliquer sur « Afficher », dans la section « Exemple de données et déroulé ». Trouver un jeu de données pour le Challenge FOOTBALL_3.
Et en fait, quand on fait des tests unitaires, c’est souvent (voir tout le temps) ce qu’on fait. C’est-à-dire qu’on sait avant de coder ce que notre code doit retourner, ce qu’il doit accomplir. On va donc suivre ce principe.
Pour simplifier, je vais avoir 3 équipes :
Et 2 matchs :
À la fin, j’aurais donc ce classement :
Et ce jeu de données va me suffire pour écrire mes tests :
$group = ['FRA', 'BEL', 'ANG'];
$scores = ['FRA_BEL_1_0', 'ANG_FRA_2_2'];
Dans le dossier /challenges/FOOTBALL_3, je crée une classe Board dans un fichier Board.php qui contiendra ces éléments :
final class Board
{
private const POINTS_START = 0;
private const POINTS_VICTORY = 3;
private const POINTS_DRAW = 1;
public array $scores = [];
public function __construct(array $group)
{
}
public function match(string $score): void
{
}
public function matchs(array $scores): void
{
}
public function getOrder(): string
{
return '';
}
}
Encore une fois, écrire ses tests avant est une bonne façon de faire.
Pour tester mon constructeur, je dois m’assurer que les scores de chaque équipe présente dans le groupe sont initialisés à 0.
Ensuite, un match a 3 issues possibles :
Je peux donc écrire ces 4 tests :
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use Challenges\FOOTBALL_3\Board;
final class Football_3Test extends TestCase
{
public function setUp(): void
{
parent::setUp();
$group = ['FRA', 'BEL', 'ANG'];
$this->Board = new Board($group);
}
// TES METHODES DE TEST ---------------------
public function test_constructeur(): void
{
$this->assertEquals(
$this->Board->scores,
['FRA' => 0, 'BEL' => 0, 'ANG' => 0]
);
}
public function test_match_domicile_gagne(): void
{
$score = 'FRA_BEL_1_0';
$this->Board->match($score);
$this->assertEquals(
$this->Board->scores,
['FRA' => 3, 'BEL' => 0, 'ANG' => 0]
);
}
public function test_match_exterieur_gagne(): void
{
$score = 'FRA_BEL_0_4'; // Dans un univers parallèle
$this->Board->match($score);
$this->assertEquals(
$this->Board->scores,
['FRA' => 0, 'BEL' => 3, 'ANG' => 0]
);
}
public function test_match_egalite(): void
{
$score = 'FRA_ANG_2_2';
$this->Board->match($score);
$this->assertEquals(
$this->Board->scores,
['FRA' => 1, 'BEL' => 0, 'ANG' => 1]
);
}
}
Un peu d’explications.
Tout d’abord, je fais référence à ma classe grâce au « use » en haut du fichier.
Dans la méthode setUp, je crée un nouveau « Board » avec le groupe de mon jeu de test.
Ensuite, j’ai mes 4 tests décrits + haut.
Maintenant, je lance mes tests avec ma console : (rappel de la commande, à la racine du projet : ./bin/vendor/phpunit –testsuite PHPUnit)
Forcément, chaque test est faux puisque mes méthodes ne sont pas encore codées.
Voici une façon d’écrire ces 2 méthodes :
final class Board
{
private const POINTS_START = 0;
private const POINTS_VICTORY = 3;
private const POINTS_DRAW = 1;
public array $scores = [];
public function __construct(array $group)
{
foreach ($group as $team) {
$this->scores[$team] = self::POINTS_START;
}
}
public function match(string $score): void
{
[$homeTeam, $awayTeam, $homeScore, $awayScore] = explode('_', $score);
if ($homeScore > $awayScore) {
$this->scores[$homeTeam] += self::POINTS_VICTORY;
} elseif ($awayScore > $homeScore) {
$this->scores[$awayTeam] += self::POINTS_VICTORY;
} else {
$this->scores[$homeTeam] += self::POINTS_DRAW;
$this->scores[$awayTeam] += self::POINTS_DRAW;
}
}
public function matchs(array $scores): void
{
}
public function getOrder(): string
{
return '';
}
}
Si je relance mes tests :
Comme mes tests sont bons, je peux essayer d’améliorer mon code. Peut-être que je peux me passer de ces if/else d’une façon ou d’une autre ?
Je vais donc tester 2 choses :
public function test_matchs(): void
{
$scores = ['FRA_BEL_1_0', 'ANG_FRA_2_2'];
$this->Board->matchs($scores);
$this->assertEquals(
$this->Board->scores,
['FRA' => 4, 'BEL' => 0, 'ANG' => 1]
);
}
public function test_classement(): void
{
$scores = ['FRA_BEL_1_0', 'ANG_FRA_2_2'];
$this->Board->matchs($scores);
$this->assertEquals(
$this->Board->getOrder(),
'FRAANGBEL' // FRA > ANG > BEL
);
}
Voici une version possible pour les méthodes matchs et getOrder :
public function matchs(array $scores): void
{
foreach ($scores as $score) {
$this->match($score);
}
// En 1 ligne :
// array_map([$this, 'match'], $scores);
}
public function getOrder(): string
{
arsort($this->scores);
return implode('', array_keys($this->scores));
}
Si je relance mes tests :
Un peu d’explications pour getOrder :
Volontairement un peu de redites avec le premier corrigé basé sur les tests unitaires.
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 le group est vide pour le constructeur, ou si le score est incomplet, par exemple « FRA_BEL_1 ».
Pour chaque type de score (domicile, extérieur, égalité), on pourrait tester 2 ou 3 versions.
On pourrait tester le classement au fur et à mesure des matchs, pour 1, 2, 3, 5, 10 matchs.
Et certains éléments de Board devraient être private et testés donc différemment.
Pour continuer dans ce sujet des tests unitaires, tu peux découvrir Les tests unitaires avec PHP #3.
Voici comment on peut résoudre le challenge dans le fichier football_3_api.php :
// CODE DU CHALLENGE ------------------
$board = new Board($group);
$board->matchs($scores);
// REPONSE ----------------------------
$game->output(['data' => $board->getOrder()]);
Le code est également disponible sur Github.
Board.php
declare(strict_types=1);
namespace Challenges\FOOTBALL_3;
final class Board
{
private const POINTS_START = 0;
private const POINTS_VICTORY = 3;
private const POINTS_DRAW = 1;
public array $scores = [];
public function __construct(array $group)
{
foreach ($group as $team) {
$this->scores[$team] = self::POINTS_START;
}
}
public function match(string $score): void
{
[$homeTeam, $awayTeam, $homeScore, $awayScore] = explode('_', $score);
if ($homeScore > $awayScore) {
$this->scores[$homeTeam] += self::POINTS_VICTORY;
} elseif ($awayScore > $homeScore) {
$this->scores[$awayTeam] += self::POINTS_VICTORY;
} else {
$this->scores[$homeTeam] += self::POINTS_DRAW;
$this->scores[$awayTeam] += self::POINTS_DRAW;
}
}
public function matchs(array $scores): void
{
foreach ($scores as $score) {
$this->match($score);
}
// En 1 ligne :
// array_map([$this, 'match'], $scores);
}
public function getOrder(): string
{
arsort($this->scores);
return implode('', array_keys($this->scores));
}
}
Football_3Test.php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use Challenges\FOOTBALL_3\Board;
final class Football_3Test extends TestCase
{
public function setUp(): void
{
parent::setUp();
$group = ['FRA', 'BEL', 'ANG'];
$this->Board = new Board($group);
}
// TES METHODES DE TEST ---------------------
public function test_constructeur(): void
{
$this->assertEquals(
$this->Board->scores,
['FRA' => 0, 'BEL' => 0, 'ANG' => 0]
);
}
public function test_match_domicile_gagne(): void
{
$score = 'FRA_BEL_1_0';
$this->Board->match($score);
$this->assertEquals(
$this->Board->scores,
['FRA' => 3, 'BEL' => 0, 'ANG' => 0]
);
}
public function test_match_exterieur_gagne(): void
{
$score = 'FRA_BEL_0_4'; // Dans un univers parallèle
$this->Board->match($score);
$this->assertEquals(
$this->Board->scores,
['FRA' => 0, 'BEL' => 3, 'ANG' => 0]
);
}
public function test_match_egalite(): void
{
$score = 'FRA_ANG_2_2';
$this->Board->match($score);
$this->assertEquals(
$this->Board->scores,
['FRA' => 1, 'BEL' => 0, 'ANG' => 1]
);
}
public function test_matchs(): void
{
$scores = ['FRA_BEL_1_0', 'ANG_FRA_2_2'];
$this->Board->matchs($scores);
$this->assertEquals(
$this->Board->scores,
['FRA' => 4, 'BEL' => 0, 'ANG' => 1]
);
}
public function test_classement(): void
{
$scores = ['FRA_BEL_1_0', 'ANG_FRA_2_2'];
$this->Board->matchs($scores);
$this->assertEquals(
$this->Board->getOrder(),
'FRAANGBEL'
);
}
}
Other content to discover
Corrections, challenges, news, technical monitoring... no spam.