Jérôme Cutrona

Version statique de l'intranet de Jérôme Cutrona - Rejoindre la version dynamique 🔒

Certains exemples et corrections sont susceptibles de ne pas être opérationnels.

B.U.T. Informatique - IUT de Reims - Université de Reims

Conception et tests autour de « Rock, Paper, Scissors, Lizard, Spock »

Navigation

Objectifs de la séance

  • Installer Composer
  • Créer un nouveau projet avec Composer
  • Installer PHPUnit
  • Découvrir PhpStorm
  • Installer des outils de tests statiques dans PhpStorm
  • Installer et configurer PHP Mess Detector (a.k.a. PHPMD)
  • Installer et configurer PHP Coding Standards Fixer (a.k.a. PHP CS Fixer)
  • Écrire des tests unitaires
  • Écrire des tests d'intégration
  • Prendre en main PHPUnit
  • Utiliser des Stubs
  • Utiliser des Mock objects
  • Découvrir l'analyse de couverture de code

Mise en place du projet

Quelques considérations liées à la configuration système de l'IUT

Remarque importante

Si vous travaillez sur votre ordinateur personnel, sur Linux ou sur Windows, vous pouvez placer le répertoire « tests-conception » du projet où bon vous semble. La partie qui suit concerne uniquement le travail sur les ordinateurs de l'IUT. Cela ne vous empêche pas d'en prendre connaissance.

Votre répertoire d'accueil « $HOME » se trouve sur un emplacement réseau NFS, ce qui vous permet de retrouver vos fichiers quel que soit le PC du département sur lequel vous vous connectez. Symfony utilise le sous-répertoire « var » du projet pour la gestion de son cache, des traces d'exécution et des données de session. Les traces d'exécution et plus particulièrement le cache sont des tâches générant de nombreuses entrées-sorties sur le système de fichiers, particulièrement inadaptées aux systèmes de fichiers en réseau. Il en va de même pour PhpStorm qui surveille les modifications de tous les fichiers de votre projet et ne peut pas fonctionner normalement lorsque ces fichiers sont sur un partage NFS. Afin d'améliorer les performances de votre application et d'éviter de saturer le serveur de fichiers du département informatique, vous allez travailler dans le répertoire « /working/votre_login » de la machine sur laquelle vous êtes connecté.

En fin de séance

En fin de séance, vous devez impérativement synchroniser votre dépôt distant (en ligne de commande ou avec PhpStorm)

git commit
puis
git push
faute de quoi vous perdrez votre travail entre chaque séance !

Pensez également à supprimer votre répertoire de travail /working/votre_login/tests-conception pour éviter la saturation du disque local :

rm -Rf /working/votre_login/tests-conception

En début de séance

Lorsque vous reprenez le travail la séance suivante, vous avez trois choix possibles, qui dépendent de la façon dont vous avez travaillé :

  • Mettre à jour votre dépôt local
    cd /working/votre_login/tests-conception
    git pull
  • Effacer votre dépôt local et repartir de zéro
    cd /working/votre_login
    rm -Rf /working/votre_login/tests-conception
    git clone https://iut-info.univ-reims.fr/gitlab/votre_login/tests-conception.git
    cd /working/votre_login/tests-conception
  • Réinitialiser votre dépôt local selon le dépôt distant
    cd /working/votre_login/tests-conception
    git fetch
    git reset --hard origin/main

Ensuite, dans le répertoire de votre projet, vous devez et (ré)installer les composants nécessaires à son fonctionnement :

composer install

Versionnage du projet

Pour chaque nouveau sujet de TP, vous créerez un nouveau répertoire, vous initialiserez un dépôt Git dans ce répertoire et créerez un dépôt distant sur GitLab. Vous réaliserez une opération de validation avec git commit au moins une fois par question du sujet.

Initialisation du dépôt

  • Ouvrez un terminal, utilisez la commande mkdir pour créer un nouveau répertoire tests-conception dans /working/votre_login et utilisez la commande cd pour vous placer dans le répertoire
  • Initialisez le dépôt Git du projet
    git init
  • Éditez le fichier .gitignore pour exclure les fichiers binaires de type image (JPEG, PNG, …), les fichiers qui contiennent des mots de passe ou les fichiers créés par votre éditeur de texte (*.swp pour vim, par exemple)
  • Éditez un fichier README.md dans lequel doivent figurer votre nom et votre prénom
  • Ajoutez les fichiers au dépôt
    git add .
  • Effectuez une première validation
    git commit -m "Initial commit"
  • Connectez-vous à l'application GitLab et créez un dépôt portant le même nom que votre nouveau répertoire
  • Accordez la permission « Reporter » à votre enseignant de TP
  • Associez le dépôt local et le dépôt distant
    git remote add origin https://iut-info.univ-reims.fr/gitlab/votre_login/tests-conception.git
  • Poussez le dépôt local vers le dépôt distant
    git push

Consignes

  • Vous effectuerez une validation après chaque question.
  • A la fin de chaque séance, vous effectuerez une validation. Si cette validation contient du code incomplet ou ne fonctionnant pas, mentionnez-le dans le message de validation. Vous pousserez ensuite votre travail vers le dépôt distant.
  • Ce dépôt sera utilisé par votre enseignant(e) de TP pour évaluer votre travail. Assurez-vous donc régulièrement que tous les fichiers que vous souhaitez lui rendre sont bien présents dans le dépôt. Respectez les noms des fichiers qui vous sont demandés si des consignes particulières vous sont données.
  • Le dépôt lui-même sera évalué : soignez l'écriture de vos messages.

L'aide-mémoire Git et l'aide-mémoire GitLab de Monsieur Nourrit pourront vous être utiles.

Nouveau projet à partir de celui du précédent sujet de TP

Afin de gagner du temps, vous allez réutiliser le projet de votre précédent sujet de TP. Vous allez donc cloner votre dépôt distant dans le répertoire de travail local, puis supprimer tous les fichiers sources et de tests pour repartir d'une base vierge, mais déjà configurée pour l'utilisation des outils de qualité de code et de tests unitaires.

Travail à réaliser
  • Placez-vous dans votre répertoire de travail dans working :
    cd /working/$USER
  • Clonez votre dépôt distant dans le répertoire tests-conception :
    git clone https://iut-info.univ-reims.fr/gitlab/$USER/test-phpunit.git tests-conception
  • Placez-vous dans le répertoire du projet cloné :
    cd tests-conception
  • Supprimez le dépôt Git du projet cloné pour repartir d'une base vierge :
    rm -Rf .git
  • Supprimez tous les fichiers sources et de tests :
    rm -Rf {src,tests}/*
  • Lancez l'installation des dépendances du projet :
    composer install
  • Vérifiez le contenu du fichier « .gitignore » pour vous assurer que les fichiers et répertoires à ne pas suivre sont correctement exclus
    cat .gitignore
    dont le résultat doit être similaire à :
    /vendor/
    
    /.idea/
    
    /.phpunit.cache
    /.php-cs-fixer.cache
  • Initialisez un nouveau dépôt Git :
    git init
  • Vérifiez le contenu de l'index Git avec la commande :
    git status
    dont le résultat doit être :
    Sur la branche main
    
    Aucun commit
    
    Fichiers non suivis:
      (utilisez "git add <fichier>..." pour inclure dans ce qui sera validé)
    	.gitignore
    	.php-cs-fixer.dist.php
    	composer.json
    	composer.lock
    	grumphp.yml
    	pdepend.xml
    	phpunit.xml
    	ruleset.xml
    
  • Lancez PhpStorm pour ouvrir le nouveau projet :
    phpstorm . &
  • Modifiez le fichier « composer.json » pour corriger le nom du projet et sa description
  • Ajoutez tous les fichiers à l'index Git :
    git add .
  • Effectuez un premier « commit » pour enregistrer l'état initial du projet et vérifiez que GrumPHP se déclenche bien
    git commit -m "Initial commit"
  • Créez un nouveau projet « tests-conception » sur https://iut-info.univ-reims.fr/gitlab/ et ajoutez le dépôt distant à votre projet local :
    git remote add origin https://iut-info.univ-reims.fr/gitlab/$USER/tests-conception.git
  • Poussez les modifications sur le dépôt distant :
    git push -u origin main

Principe du jeu et de la réalisation

Vous allez vous intéresser au développement d'un jeu « Rock, Paper, Scissors » et ses évolutions possibles à travers l'ajout de nouveaux gestes comme dans la variante « Rock, Paper, Scissors, Lizard, Spock ». Le jeu sera exécuté en PHP ligne de commande.

Le principe de ces jeux repose sur l'opposition de deux joueurs qui choisissent chacun un geste parmi 2n+1. Chaque geste l'emporte contre n gestes et perd contre n autres gestes. Si les deux joueurs choisissent le même geste, il y a égalité.

Les combinaisons gagnantes pour « Rock, Paper, Scissors » (n=1) sont triviales : Rock Paper Scissors Les combinaisons gagnantes pour « Rock, Paper, Scissors, Lizard, Spock » (n=2) sont : Rock Paper Scissors Lizard Spock Vous pouvez consulter des variantes à 7, 9, 11, 15, 25 voire 101 gestes.

La réalisation de ces jeux va se faire, en fonction des spécifications, selon les bonnes pratiques de conception afin de permettre flexibilité, robustesse et évolutivité, avec une implémentation réfléchie et des tests garantissant le bon fonctionnement des composants et de l'ensemble.

Rock, Paper, Scissors

Pour commencer, la réalisation va se limiter à la variante la plus simple du jeu avec trois gestes.

Gestion des règles

Une partie du fonctionnement du jeu réside dans la détermination du geste gagnant. Lorsque les gestes des deux opposants sont confrontés, si le premier est gagnant, il remporte 1 point, s'il est perdant, il perd 1 point et en cas d'égalité, le résultat est nul.

Travail à réaliser
  1. Observez le diagramme de classes Diagramme des classes associées aux règles
    Le diagramme précédent est celui des classes associées aux règles.
  2. Créez l'interface et les classes avec l'assistant de PhpStorm
  3. Écrivez les méthodes de la classe « RockPaperScissors »
    • « getGestures() » donne l'ensemble des gestes
    • « compare() » retourne 1, -1 ou 0 selon que « gesture1 » gagne, perd ou est à égalité avec « gesture2 »
    • « checkGesture() » vérifie que son paramètre est un geste valide
    • « gestureIndex() » donne l'indice du geste dans le tableau « GESTURES » afin de faciliter l'évaluation de la comparaison (cette méthode n'est pas obligatoire, vous pouvez réaliser « compare() » sans elle)
  4. Écrivez les tests de la classe « RockPaperScissors »
    Remarque importante

    Les classes de test font partie de l'espace de noms « Tests » suivi de l'espace de noms de la classe qu'elles testent.

Les joueurs

Les joueurs, machine ou humain, peuvent jouer un coup valide. Diagramme des classes associées aux joueurs

Le diagramme précédent est celui des classes associées aux joueurs.
Travail à réaliser
  1. Observez le diagramme de classes
  2. Créez les classes et interface avec PhpStorm
  3. Écrivez les méthodes de la classe « Player »
  4. Écrivez la méthode de la classe « Computer » qui réalise un tirage aléatoire parmi l'ensemble des gestes connus dans les règles
  5. Écrivez les méthodes de la classe « Human »
    • « showPossibilities() » retourne l'ensemble des gestes possibles sous forme d'une chaîne de caractères
    • « prompt() » affiche les gestes possibles et invite l'utilisateur à en choisir un
    • « getInput() » effectue une lecture au clavier et la retourne
    • « playOneMove() » qui effectue toutes les opérations nécessaires pour récupérer un geste valide du joueur
  6. Écrivez les tests des classes « Human » et « Computer »
    Remarque importante

    Les classes de test font partie de l'espace de noms « Tests » suivi de l'espace de noms de la classe qu'elles testent.

    Namespaces

    La classe « Human » produisant une sortie écran, lisez la documentation concernant ce point.

Le jeu

Le jeu nécessite deux joueurs et un jeu de règles. Diagramme des classes associées au jeu

Le diagramme précédent est celui des classes associées au jeu.
Travail à réaliser
  1. Observez le diagramme de classes
  2. Créez la classe avec PhpStorm
  3. Écrivez les méthodes de la classe « Game »
    • le constructeur
    • « play() » qui lance la partie pour un certain nombre de tours minimum jusqu'à obtenir un vainqueur et qui cumule dans « score » les résultats obtenus par « playOneMove() »
    • « playOneMove() » qui correspond à un tour de jeu
    • « getScore() »
    • « winner() » qui retourne le vainqueur ou lève une « RuntimeException » s'il n'y en a pas
  4. Écrivez les tests de la classe « Game » en utilisant les exécutions successives simulées (« willReturnOnConsecutiveCalls() ») de la méthode « playOneMove() » pour simuler une partie complète
    Namespaces

    Les classes de test font partie de l'espace de noms « Tests » suivi de l'espace de noms de la classe qu'elles testent.

    Sortie écran

    Votre programme produit une sortie écran qui va polluer l'affichage du résultat de PHPUnit, mais également lui faire considérer les tests comme risqués. Une solution consiste à mettre des attentes (expectations en anglais) de sortie écran dans vos tests, avec une contrainte minimale sur ces attentes. Ainsi, l'attente suivante devrait être vérifiée par tous les tests dont la sortie écran est non vide et que nous ne souhaitons pas tester précisément :

    $this->expectOutputRegex('/./');

Couverture de code

Tous les composants nécessaires étant présents et testés, il est utile de vérifier la couverture de code.

Travail à réaliser
  1. Générez un rapport d'analyse de couverture de code dans le répertoire « coverage »
  2. Excluez ce répertoire coverage de votre dépôt Git
  3. Ajoutez un nouveau script Composer « test:coverage » qui efface le répertoire « coverage » avant de lancer la commande PHPUnit que vous venez d'élaborer
    Information

    À la suite de la commande d'évaluation de la couverture de code, vous pouvez ouvrir automatiquement le résultat « coverage/index.html » dans votre navigateur Web à l'aide d'une commande :

  4. Vérifiez que votre couverture de code est suffisante

Programme pour jouer

Tous les composants nécessaires étant présents, il reste à pouvoir jouer.

Travail à réaliser
  1. Créez un répertoire « bin » dans lequel vous mettrez vos programmes
  2. Dans le répertoire « bin », créez un programme « playRPS » qui commence par :
    #!/usr/bin/env php
    <?php
    Information

    La première ligne du script est le « shebang » qui indique que le fichier est un script et qu'il doit ici être exécuté par l'interpréteur « php » à trouver dans les chemins donnés par la variable d'environnement « PATH ».

    Vous pouvez lancer vos programmes avec :

    php bin/nom_programme
  3. Rendez le programme « playRPS » exécutable
    Information

    Vous pouvez à présent lancer vos programmes avec :

    bin/nom_programme
  4. Ajoutez au début du programme l'inclusion de l'auto-chargement de Composer en utilisant la constante magique « __DIR__ » pour déterminer le chemin du programme
  5. Écrivez le programme « playRPS » qui permet à un humain « Bob » de jouer contre la machine « HAL » pour 3 tours minimum, qui pourrait donner :
    bin/playRPS
    Saisissez votre geste (R, P, S) : R
    R R --> 0
    Saisissez votre geste (R, P, S) : o
    Saisissez votre geste (R, P, S) : R
    R S --> 1
    Saisissez votre geste (R, P, S) : R
    R P --> -1
    Saisissez votre geste (R, P, S) : P
    P R --> 1
    Bob won!
    

Rock, Paper, Scissors, Lizard, Spock

Vous allez reproduire ce qui vient d'être fait pour « Rock, Paper, Scissors » afin d'arriver à la création des composants nécessaires à la variante « Rock, Paper, Scissors, Lizard, Spock ».

Travail à réaliser
  1. Dupliquez la classe « RockPaperScissors » en « RockPaperScissorsLizardSpock »
  2. Complétez les gestes possibles dans les constantes de classe : « 'R' », « 'P' », « 'S' », « 'L' », « 'V' » (« 'V' » pour « Vulcan Salute » 🖖)
  3. Vous pouvez construire un tableau des gestes gagnants sur le modèle « Rock gagne face à Scissors et Lizard », …
  4. Donnez une version fonctionnelle de la méthode « compare() »
  5. Écrivez les tests permettant de le vérifier
  6. Écrivez le programme « playRPSLV » qui permet à un humain « Bob » de jouer contre la machine « HAL » pour 3 tours minimum

Généralisation et refonte

En fonction de la façon dont vous avez réalisé la variante « Rock, Paper, Scissors, Lizard, Spock », votre code est plus ou moins redondant par rapport à « Rock, Paper, Scissors ». Qu'en sera-t-il pour les variantes comportant plus de gestes ?

En réorganisant astucieusement les gestes en « Rock, Spock, Paper, Lizard, Scissors », les scores associés aux combinaisons sont :

RVPLS
R0-1-111
V10-1-11
P110-1-1
L-1110-1
S-1-1110

Dans une vision cyclique des gestes, pour un geste donné, les deux suivants l'emportent alors que les deux précédents sont battus : Rock Paper Scissors Lizard Spock

La généralisation à 2n+1 gestes donne les n gestes suivants vainqueurs et les n gestes précédents perdants. Le fonctionnement réside alors sur l'ordre des gestes qui permet de connaître les points pour chaque confrontation, ce qui va entrainer la création d'une nouvelle hiérarchie de classes.

Les gestes

Vous allez séparer les gestes des règles : Diagramme des classes associées aux gestes

Le diagramme précédent est celui des classes associées aux gestes.
Travail à réaliser
  1. Créez une interface « MyGame\Rules\Gestures\IGestures » représentant les gestes associés à des règles
  2. Ajoutez une méthode « getGestures() » à votre nouvelle interface
  3. Parcourez rapidement la documentation de « ReflectionClass » et lisez attentivement celle de sa méthode « getConstants() »
  4. Créez une classe abstraite « MyGame\Rules\Gestures\Gestures » qui implémente « IGestures » et qui aura pour but de retourner les constantes des classes qui dériveront d'elle
  5. Lisez la documentation concernant la résolution statique à la volée (« Late Static Bindings »)
  6. Donnez le code suivant à la méthode « getGestures() » :
    $reflectionClass = new ReflectionClass(static::class);
    return array_values($reflectionClass->getConstants());
  7. Testez votre classe abstraite « Gestures » en créant une classe anonyme en héritant
  8. Dérivez la classe abstraite « Gestures » en « RockPaperScissorsLizardSpockGestures »
  9. Ajoutez les constantes des gestes à votre nouvelle classe
  10. Testez « RockPaperScissorsLizardSpockGestures »

Les règles

Les règles vont également être revues : Diagramme des classes associées aux règles généralisées

Le diagramme précédent est celui des classes associées aux règles généralisées.
Travail à réaliser
  1. Créez avec PhpStorm la classe « GenericRules » qui implémente « IRules »
  2. Ajoutez à « GenericRules » un constructeur qui reçoit une instance de « IGestures » qui sera mémorisée dans une propriété d'instance
  3. En vous appuyant sur les principes énoncées au début de cette partie, donnez une version fonctionnelle des méthodes :
    • « getGestures() »
    • « checkGesture() »
    • « compare() »
    • « getGestureIndex() »
  4. Modifiez la classe « RockPaperScissorsLizardSpock » afin qu'elle hérite de « GenericRules » et qu'elle ne comporte plus qu'un constructeur
  5. Adaptez éventuellement les tests de « RockPaperScissorsLizardSpock » et vérifiez qu'ils passent
  6. Modifiez la classe « RockPaperScissors » afin qu'elle hérite de « GenericRules » et qu'elle ne comporte plus qu'un constructeur
  7. Adaptez éventuellement les tests de « RockPaperScissors » et vérifiez que votre classe les passe

Couverture de code

Tous les composants nécessaires étant présents et testés, il est utile de vérifier la couverture de code.

Travail à réaliser
  1. Générez un rapport d'analyse de couverture de code dans le répertoire « coverage »
  2. Vérifiez que votre couverture de code est suffisante
Remarque importante

Vous devrez fixer le mode de fonctionnement de Xdebug en préfixant votre commande de calcul de couverture de code par l'affectation de la variable d'environnement XDEBUG_MODE avec la valeur « coverage » :

XDEBUG_MODE=coverage vendor/bin/phpunit

RPS-9

Mettons votre code à l'épreuve de RPS-9.

Travail à réaliser
  1. Téléchargez le code de la classe de gestes RockGunWaterAirPaperSpongeHumanScissorsFireGestures.php (télécharger)
  2. Téléchargez le code de la classe de règles RockGunWaterAirPaperSpongeHumanScissorsFire.php (télécharger)
  3. Téléchargez le code de la classe de tests RockGunWaterAirPaperSpongeHumanScissorsFireTest.php (télécharger)
  4. Vérifiez que vos classes passent ces nouveaux tests