Testez vos règles personnalisées PHPStan

Publié le 26 mars 2024 par Mathieu De Gracia
Couverture de l'article Testez vos règles personnalisées PHPStan

De PHPStan à Rector en passant par Pint, l'analyse statique n'a jamais été aussi présente et accessible dans nos projets Laravel.

Que ce soit pour détecter des anomalies ou corriger automatiquement des erreurs, ces outils sont devenus ces dernières années des alliés essentiels pour améliorer la qualité de nos projets.

Parmi les plus populaires d'entre eux, PHPStan identifie les bugs de votre application avant qu'ils n'arrivent en production en effectuant des analyses de votre code à l'aide d'un ensemble de règles.

PHPStan offre également la possibilité de créer vos propres règles de validation, voyons dans cet article comment tester unitairement l'une de ces règles personnalisées !

Créer une règle personnalisée

Dans le cadre de ce tutoriel, nous allons créer une règle personnalisée que nous allons par la suite tester unitairement.

Si vous voulez en savoir plus sur le fonctionnement de PHPStan, nous vous conseillons notre article sur comment créer vos propres règles avec le package.

Imaginons que nous souhaitions interdire l'écriture de méthode privée dans notre application, cela est facilement vérifiable avec la règle personnalisée suivante :

1<?php
2 
3namespace App\Rules;
4 
5use PhpParser\Node;
6use PHPStan\Analyser\Scope;
7use PHPStan\Rules\Rule;
8use PHPStan\Node\InClassMethodNode;
9 
10class ClassesHaveNoPrivateMethodsRule implements Rule
11{
12 public function getNodeType(): string
13 {
14 return InClassMethodNode::class;
15 }
16 
17 public function processNode(Node $node, Scope $scope): array
18 {
19 $methodReflection = $node->getMethodReflection();
20 
21 return $methodReflection->isPrivate()
22 ? ['No private method allowed.']
23 : [];
24 }
25}

Une fois la règle écrite, il faut désormais la référencer auprès de PHPStan pour quelle soit prise en compte lors des analyses, pour se faire, il sera nécessaire de créer un fichier de configuration à l'aide de la commande suivante :

1touch phpstan.neon.dist

Dans ce fichier, renseignez les quelques informations suivantes ainsi que le chemin vers votre règle dans la section rules :

1rules:
2 - App\Rules\ClassesHaveNoPrivateMethodsRule
3
4parameters:
5
6 paths:
7 - app

Vous pouvez dès lors analyser votre projet à l'aide de votre règle en exécutant la commande :

1vendor/bin/phpstan

Félicitations, vous venez d'analyser votre projet à l'aide d'une règle personnalisée ... cependant, comment s'assurer de son bon fonctionnement ? Peut-être que votre règle comporte des bugs, nous avons peut-être fait des erreurs ?

Il est important de pouvoir s'assurer du bon fonctionnement d'une règle personnalisée avant de l'utiliser en production, voyons désormais ce que propose PHPStan afin de tester unitairement une règle !

Tester unitairement la règle

Tout d'abord, créons le fichier de test associé à notre règle :

1namespace Tests\Unit;
2 
3use PHPStan\Testing\RuleTestCase;
4 
5final class ClassesHaveNoPrivateMethodsRuleTest extends RuleTestCase
6{
7 //
8}

Ce fichier de test sera semblable à n'importe quel test unitaire de votre projet Laravel à la seule différence qu'il devra étendre de la classe RuleTestCase.

Ce testcase est celui fourni par PHPStan et contient un ensemble de méthodes permettant de tester unitairement une règle personnalisée.

Votre fichier de test devra également implémenter une méthode getRule retournant une instance de la règle que vous souhaitez manipuler :

1 
2namespace Tests\Unit;
3 
4use PHPStan\Testing\RuleTestCase;
5use PHPStan\Rules\Rule;
6use App\Rules\ClassesHaveNoPrivateMethodsRule;
7 
8final class ClassesHaveNoPrivateMethodsRuleTest extends RuleTestCase
9{
10 protected function getRule(): Rule
11 {
12 return new ClassesHaveNoPrivateMethodsRule();
13 }
14}

Maintenant, ajoutons les méthodes correspondant aux différents cas d'utilisation que nous chercherons à valider par la suite.

Notre règle contient deux cas d'utilisation : elle peut soit détecter une méthode privée dans une classe et lever une erreur, soit ne rien faire dans le cas contraire.

1 
2namespace Tests;
3 
4use PHPStan\Testing\RuleTestCase;
5use PHPUnit\Framework\Attributes\Test;
6use App\Rules\ClassesHaveNoPrivateMethodsRule;
7use PHPStan\Rules\Rule;
8 
9final class ClassesHaveNoPrivateMethodsRuleTest extends RuleTestCase
10{
11 protected function getRule(): Rule
12 {
13 return new ClassesHaveNoPrivateMethodsRule();
14 }
15 
16 #[Test]
17 public function it_not_raise_an_error_if_all_the_methods_are_public(): void
18 {
19 
20 }
21 
22 #[Test]
23 public function it_raises_an_error_if_it_detects_a_private_method(): void
24 {
25 
26 }
27}

Une règle PHPStan vérifie du code en le lisant, le test unitaire n'y fait pas exception ... pour répondre à cette contrainte, nous pouvons écrire des stubs contenant le code que nous allons par la suite soumettre à nos cas de test :

1touch tests/stubs/ClassWithPublicMethod.php
2touch tests/stubs/ClassWithPrivateMethod.php

Le stub est un pattern faisant partie de l'attirail des doublures au même titre que les mocks, les spies ou bien encore les dummies.

Dans notre cas, un stub est un fichier écrit en dur, ne possédant pas de logique métier, permettant de simuler un objet répondant à un cas d'utilisation.

Le premier de ces stubs sera une classe sans méthode privée :

1class ClassWithPublicMethod
2{
3 public function foo()
4 {
5 //
6 }
7}

Tandis que le second stub sera une classe contenant une méthode privée :

1class ClassWithPrivateMethod
2{
3 private function bar()
4 {
5 //
6 }
7}

Maintenant que nous possédons nos stubs, nous pouvons compléter les différentes méthodes de test.

Le testcase de PHPStan offre plusieurs méthodes permettant de simuler le lancement d'une analyse.

La première de ces méthodes, gatherAnalyserErrors, reçoit en argument le chemin des fichiers à tester, dans notre cas, le stub ClassWithPublicMethod et retournera un array contenant les erreurs identifiées par la règle :

1 
2namespace Tests;
3 
4use PHPStan\Testing\RuleTestCase;
5use PHPUnit\Framework\Attributes\Test;
6use App\Rules\ClassesHaveNoPrivateMethodsRule;
7use PHPStan\Rules\Rule;
8 
9final class ClassesHaveNoPrivateMethodsRuleTest extends RuleTestCase
10{
11 protected function getRule(): Rule
12 {
13 return new ClassesHaveNoPrivateMethodsRule();
14 }
15 
16 #[Test]
17 public function it_not_raise_an_error_if_all_the_methods_are_public(): void
18 {
19 $errors = $this->gatherAnalyserErrors([
20 __DIR__ . '/stubs/ClassWithPublicMethod.php'
21 ]);
22 
23 $this->assertCount(0, $errors);
24 }
25 
26 #[Test]
27 public function it_raises_an_error_if_it_detects_a_private_method(): void
28 {
29 //
30 }
31}

PHPUnit est disponible afin de valider des assertions !

La seconde méthode proposée par PHPStan, analyse, reçoit également en premier argument les fichiers à analyser ainsi qu'un tableau en second argument contenant les erreurs que la règle doit détecter !

1 
2namespace Tests;
3 
4use PHPStan\Testing\RuleTestCase;
5use PHPUnit\Framework\Attributes\Test;
6use App\Rules\ClassesHaveNoPrivateMethodsRule;
7use PHPStan\Rules\Rule;
8 
9final class ClassesHaveNoPrivateMethodsRuleTest extends RuleTestCase
10{
11 protected function getRule(): Rule
12 {
13 return new ClassesHaveNoPrivateMethodsRule();
14 }
15 
16 #[Test]
17 public function it_not_raise_an_error_if_all_the_methods_are_public(): void
18 {
19 $errors = $this->gatherAnalyserErrors([
20 __DIR__ . '/stubs/ClassWithPublicMethod.php'
21 ]);
22 
23 $this->assertCount(0, $errors);
24 }
25 
26 #[Test]
27 public function it_raises_an_error_if_it_detects_a_private_method(): void
28 {
29 $this->analyse(
30 [__DIR__ . '/stubs/ClassWithPrivateMethod.php'],
31 expectedErrors: [
32 [
33 'No private method allowed.',
34 10,
35 ],
36 ],
37 );
38 }
39}

L'argument expectedErrors est un tableau multidimensionnel où la première valeur de chaque ligne correspond au message d'erreur et la seconde valeur correspond à la ligne correspondante dans le fichier analysé.

Maintenant que notre fichier de test est complet et opérationnel, lancez vos tests unitaires à l'aide de la commande suivante :

1vendor/bin/phpunit tests/Unit/ClassesHaveNoPrivateMethodsRuleTest.php

Félicitation, vous savez désormais comment tester unitairement des règles PHPStan !

Mathieu De Gracia avatar
Mathieu De Gracia
Des fois, mon chat code à ma place 🐱

A lire

Autres articles de la même catégorie