Un projet cohérent sur le long terme grâce aux tests d'architecture

L’architecture d’un projet joue un rôle crucial dans sa maintenabilité et son évolution. Si généralement au début d’un projet, il est facile de bien structurer son code, l’ajout successif de nouvelles fonctionnalités, l’arrivée ou le passage de nouveaux développeurs peuvent dégrader la qualité des fondations du projet au cours du temps. C’est dans ce cadre que les tests d’architecture vont nous aider à documenter et à garantir la cohérence de notre architecture sur le long terme.

Les tests d’architecture permettent de vérifier que le code d’un projet respecte les règles et contraintes architecturales qui ont été définies. Ils sont là pour s’assurer que le code et ses interactions sont correctement structurés. Ils permettent de maintenant la cohérence du code au fil des évolutions. Si des règles sont enfreintes, les tests permettront de relever les erreurs.

De plus, ces tests pourront également servir de documentation, puisqu’il sera nécessaire de matérialiser explicitement les règles applicables. Cela permettra ainsi de faciliter l’intégration des nouveaux développeurs qui pourront prendre connaissance des règles d’architecture du projet tout en ayant un filet de sécurité qui les empêcheront de faire des erreurs.

Pour mettre en place ce type de tests, il existe une multitude d’outils. Je vais dans ce billet en présenter quelques-uns provenant de l’écosystème PHP.

Parmi les outils existants, l’un des plus anciens (et peut-être l’un des plus connus) est certainement Deptrac. Il s’agit d’un outil d’analyse statique qui permet de définir des règles de dépendance entre les différentes couches de code qui sont ensuite vérifiées par l’outil. La configuration s’effectue au travers d’un fichier YAML (ou PHP), comme celui-ci :

paths:
  - ./src
exclude_files:
  - '#.*test.*#'
layers:
  - name: Controller
    collectors:
      - type: className
        regex: .*\\Controller\\.*
  - name: Service
    collectors:
      - type: className
        regex: .*\\Service\\.*
  - name: Repository
    collectors:
      - type: className
        regex: .*\\Repository\\.*
ruleset:
  Controller:
    - Service
  Service:
    - Repository
  Repository: ~

On peut également présenter PHPArkitect comme alternative à Deptrac. Tout comme Deptrac, PHPArkitect permet de faire respecter les règles d’architecture. Ce dernier se distingue par son approche de configuration qui se veut expressive via du code PHP.

use Arkitect\ClassSet;
use Arkitect\CLI\Config;
use Arkitect\Expression\ForClasses\HaveNameMatching;
use Arkitect\Expression\ForClasses\NotHaveDependencyOutsideNamespace;
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
use Arkitect\Rules\Rule;

return static function (Config $config): void {
    $mvcClassSet = ClassSet::fromDir(__DIR__.'/mvc', __DIR__.'/lib/my-lib/src');

    $rules = [];

    $rules[] = Rule::allClasses()
        ->that(new ResideInOneOfTheseNamespaces('App\Controller'))
        ->should(new HaveNameMatching('*Controller'))
        ->because('we want uniform naming');

    $rules[] = Rule::allClasses()
        ->that(new ResideInOneOfTheseNamespaces('App\Domain'))
        ->should(new NotHaveDependencyOutsideNamespace('App\Domain'))
        ->because('we want protect our domain');

    $config
        ->add($mvcClassSet, ...$rules);
};

Il existe également PHPat (alias PHP Architecture Tester) qui est un outil basé sur PHPStan. L’approche de configuration est très similaire à l’outil précédent, la différence essentielle étant que PHPArkitect peut fonctionner de manière autonome, alors que pour utiliser PHPat, il sera nécessaire d’installer PHPStan.

use PHPat\Selector\Selector;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;
use App\Domain\SuperForbiddenClass;

final class MyFirstTest
{
    public function test_domain_does_not_depend_on_other_layers(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::inNamespace('App\Domain'))
            ->shouldNotDependOn()
            ->classes(
                Selector::inNamespace('App\Application'),
                Selector::inNamespace('App\Infrastructure'),
                Selector::classname(SuperForbiddenClass::class),
                Selector::classname('/^SomeVendor\\\.*\\\ForbiddenSubfolder\\\.*/', true)
            )
            ->because('this will break our architecture, implement it another way! see /docs/howto.md');
    }
}

Le dernier outil que je vais présenter pour faire des tests d’architecture est Pest. Pest est un outil largement utilisé dans l’écosystème Laravel. S’il a été initialement conçu pour écrire des tests unitaires/fonctionnels, il permet également de vérifier les règles architecturales d’un projet. L’avantage d’utiliser ce dernier est d’avoir un outil qui permet à la fois de gérer vos tests unitaires et d’architecture en utilisant une syntaxe similaire.

arch()
    ->expect('App')
    ->toUseStrictTypes()
    ->not->toUse(['die', 'dd', 'dump']);
arch()
    ->expect('App\Models')
    ->toBeClasses()
    ->toExtend('Illuminate\Database\Eloquent\Model')
    ->toOnlyBeUsedIn('App\Repositories')
    ->ignoring('App\Models\User');
arch()
    ->expect('App\Http')
    ->toOnlyBeUsedIn('App\Http');
arch()
    ->expect('App\*\Traits')
    ->toBeTraits();
arch()->preset()->php();
arch()->preset()->security()->ignoring('md5');

Peu importe l’outil que vous utiliserez (ce n’est finalement qu’un détail d’implémentation), les tests d’architecture représentent une étape cruciale de vos pratiques de développement. Grâce aux outils présentés dans cet article, vous aurez la possibilité de garantir le respect de vos règles d’architecture, limitant ainsi les dérives naturelles qui arrivent au cours de la vie d’un projet.

En plus de la garantie que vos règles soient respectées, la mise en place de ce genre de tests vous permettra de rendre les règles explicites, pouvant ainsi servir de documentation. Le Graal étant bien entendu que ces tests soient lancés dans un environnement d’intégration continue.