🔬

Pestを使ってアーキテクチャテストをやってみる

2024/02/06に公開

はじめに

PestっていうPHPのテスティングフレームワークがあります。

https://pestphp.com/

そんなPestにはアーキテクチャテストが含まれています。

https://pestphp.com/docs/arch-testing

そこで今回はPestを使ってアーキテクチャテストを試してみたいと思います。

環境

  • PHP 8.3.2
  • Pest 2.33.4

インストール

Pestのインストールは次の通り

composer require pestphp/pest --dev --with-all-dependencies

ルールを決める

アーキテクチャテストを試すために適当にルール(依存関係など)を決めておきます。
今回は下記のようなLaravelのディレクトリ構造にServicesディレクトリを追加した状態でやってみます。
※ちなみにLaravelはバージョン10.43.0を使います。

app
├── Http
│   ├── Controllers
│   │   └── UserController.php
├── Models
│   └── User.php
└── Services
    └── UserService.php

サンプルコードとルールは次の通りです。

サンプルコード

app/Http/Controllers/UserController.php
<?php

namespace App\Http\Controllers;

use App\Services\UserService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class UserController
{
    public function __invoke(Request $request, UserService $service): JsonResponse
    {
        $service->create($request->input('name'));

        return new JsonResponse([], 201);
    }
}
app/Services/UserService.php
<?php

namespace App\Services;

use App\Models\User;

class UserService
{
    public function create(string $name): void
    {
        $user = new User();
        $user->name = $name;
        $user->save();
    }
}
app/Models/User.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
    ];
}

app/Models/User.phpは初めから用意された内容を適当に削っています。

ルール

  • Controllers
    • クラス名の末尾はControllerにする
    • Illuminate\Http\Requestが使えるのはControllersのみ
  • Services
    • クラス名の末尾はServiceにする
    • Servicesが使えるのはControllersでのみ
  • Models
    • Illuminate\Database\Eloquent\Modelを継承している
    • Modelsが使えるのはServicesでのみ

最終的には、Controllers -> Services -> Models の順番に依存している形をテストしたいと思います。

テストを書く

ルールを決めたので実際にテストを書いていきます。
Pestはテストクラスを定義する必要もないので、今回はtests/Architect.phpを作って、そこに書いていきます。

tests/Architect.php
<?php

describe('Controllers', function() {
    arch('クラス名の末尾は`Controller`にする')
        ->expect('App\Http\Controllers')
        ->toHaveSuffix('Controller');

    arch('`Illuminate\Http\Request`が使えるのはControllersのみ')
        ->expect('App\Http\Controllers')
        ->toOnlyBeUsedIn('Illuminate\Http\Request');
});

describe('Services', function() {
    arch('クラス名の末尾は`Service`にする')
        ->expect('App\Services')
        ->toHaveSuffix('Service');

    arch('Servicesが使えるのはControllersでのみ')
        ->expect('App\Services')
        ->toOnlyBeUsedIn('App\Http\Controllers');
});

describe('Models', function() {
    arch('`Illuminate\Database\Eloquent\Model`を継承している')
        ->expect('App\Models')
        ->toExtend('Illuminate\Database\Eloquent\Model');

    arch('Modelsが使えるのはServicesでのみ')
        ->expect('App\Models')
        ->toOnlyBeUsedIn('App\Services');
});

今回使った処理を簡単に書くとこんな感じになる。

  • describe関数を使えば関連するテストをグループ化することもできるので、今回それぞれのディレクトリでまとめた
  • arch関数を使ってルールにあったテストを書いていく
    • test関数を使っても同じことができるが、アーキテクチャテストだからarch関数を使ってみた
    • expectメソッドを使えばルールをチェックするネームスペースを指定できる
    • toHaveSuffixメソッドでクラス名の末尾が指定した文字列か検証
    • toOnlyBeUsedInメソッドで指定したネームスペースがexpectメソッドで指定した箇所でのみ使われているか検証
    • toExtendメソッドで指定したクラスがexpectメソッドで指定した箇所のクラスが継承しているか検証

テストを実行する

下記のコマンドを実行すれば、今回作成したテストの確認ができます。

% ./vendor/bin/pest tests/Architect.php

   PASS  Tests\Architect
  ✓ Controllers → クラス名の末尾はControllerにする
  ✓ Controllers → Illuminate\Http\Requestが使えるのはControllersのみ
  ✓ Services → クラス名の末尾はServiceにする
  ✓ Services → Servicesが使えるのはControllersでのみ
  ✓ Models → Illuminate\Database\Eloquent\Modelを継承している
  ✓ Models → Modelsが使えるのはServicesでのみ

  Tests:    6 passed (6 assertions)
  Duration: 0.07s

それらしく動きましたね。
試しにControllersのところでModelsを使うように変更してから実行してみます。

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class UserController
{
    public function __invoke(Request $request): JsonResponse
    {
        $user = new User();
        $user->name = $request->input('name');
        $user->save();

        return new JsonResponse([], 201);
    }
}
% ./vendor/bin/pest tests/Architect.php

   FAIL  Tests\Architect
  ✓ Controllers → クラス名の末尾はControllerにする
  ✓ Controllers → Illuminate\Http\Requestが使えるのはControllersのみ
  ✓ Services → クラス名の末尾はServiceにする
  ✓ Services → Servicesが使えるのはControllersでのみ
  ✓ Models → Illuminate\Database\Eloquent\Modelを継承している
  ⨯ Models → Modelsが使えるのはServicesでのみ
────────────────────────────────────────────────────────────────────
   FAILED  Tests\Architect > `Models` → Modelsが使えるのはServicesでのみ                                                                                                                ArchExpectationFailedException   
  Expecting 'App\Models\User' not to be used on 'App\Http\Controllers\UserController'.

  at app/Http/Controllers/UserController.php:5
      1<?php
      23▕ namespace App\Http\Controllers;
      4▕ 
  ➜   5▕ use App\Models\User;
      6▕ use Illuminate\Http\JsonResponse;
      7▕ use Illuminate\Http\Request;
      89▕ class UserController

  1   app/Http/Controllers/UserController.php:5


  Tests:    1 failed, 5 passed (9 assertions)
  Duration: 0.07s

無事にエラーになりましたね。

まとめ

今回はPestを使ってアーキテクチャテストを試してみました。
PHPで書けるってこともあるのでわかりやすいかなって思いました。(好みは出てくるかもしれないですが...)

ただ、PHPUnitを使っているプロジェクトならアーキテクチャテストのためにPestを導入する必要もないかもしれないですが(Deptracやphpatを使う方がいいかもしれない)、テストも含めてPestを使うプロジェクトなら導入してもいいかもしれないですね。

ほかにもいろいろなルールがあるのでドキュメントを見ながら試してみたいと思います。

Discussion