💯

Pest Test ってどうなん?2Weekたったあとの所感

2022/10/28に公開

実装フェーズに入ってウキウキのPHPerです。

Laravel で開発するプロジェクトがメインですが、CI/CDを導入している案件であるためにTDDでのテストコード開発も並走していて、PHPによるフィーチャーテストを導入しています。

Laravel、というかPHPでのTDDといえば「PHPUnit」だったわけですが、Laravel 9 もサポートしている「Pest」を使ってみたので、今のところの所感を書いていきます。

Pest とは?

洗練された PHP テストフレームワーク

Pest は、シンプルさに重点を置いたテストフレームワークです。
PHP にテストの楽しさをもたらすために慎重に作成されました。

人々が愛するインターフェース

Pest は、コンソールから直接、世界で最も美しいテストレポートを提供します!
また、読み取り可能なエラーとスタックトレースにより、デバッグが非常に高速になります。

https://pestphp.com/

相変わらずのポエマーでして。はい。

良いところ

  1. 「御作法」が少ない
  2. JavaScript テストフレームワーク「Jest」と互換性あり
  3. バリデーションチェックしやすい
  4. テスト結果がわかりやすい
  5. PHPUnitのアサーションもサポートしている

うーん、と思ったところ

  1. 結局、PHPUnit的な検証方法になっている
  2. 結局、PHPUnitやTDDライブラリのドキュメントを見ることになる
  3. テスト実行速度とかはあんまり変わらない

結論

  • フロントエンドに JavaScript テストフレームワークを利用しているプロジェクトだったら導入はしやすいかも
  • 結局のところ「テスト設計」次第での、品質や学習コストなので、全てPestが解決してくれるという考えは良くない
  • だから、PHPUnit的な書き方になるし、TDD工程における劇的な変化を感じない

👍 「御作法」が少ない

よく文献でも見るやつですね。
テストClassを定義していくのではなく、クロージャー関数を組み合わせて作っていく感じなので、宣言や型、みたいなことを気にせずに書けちゃう代物になってます。

クロージャーを駆使していくのも、またJSライクといったところでしょうか。これだけ見ると、まぁ簡潔です。

PHPUnitで書く

# PHPUnitの場合

use Tests\TestCase;

class PhpUnitTest extends TestCase
{
    protected $hoge;

    public function setUp(): void
    {
        parent::setUp();
        $this->hoge = 'テスト開始';
    }

    public function tearDown(): void
    {
        parent::tearDown();
        $this->hoge = 'テスト終了';
    }

    public function test_PHPユニットでテストができる(): void
    {
        $response = $this->post(
            route('message.store'),
            ['message' => $this->hoge]
        );

        $response->assertStatus(200);
    }
}

Pestで書く

# Pestの場合

beforeEach(function () {
    $this->hoge = 'テスト開始';
});

afterEach(function () {
    $this->hoge = 'テスト終了';
});

test('Pestでテストができる', function () {
    $response = $this->post(
        route('message.store'),
        ['message' => $this->hoge]
    );

    $response->assertSuccessful();
});

👍 Jestと記法や文法に互換性あり

多分 PHP + Jest = Pest なんだろうな、と思ってます。
僕が所属しているプロジェクトでの最大のメリットというところでした。フロントエンドアーキテクチャーとの親和性が高い(学習コスト減)というところが魅力的ですよね。

書いている言葉が違うだけで、文法的には酷似している印象です。

Jestで書く

// Jestテスト

test("要素があるかの確認", () => {
  expect(wrapper.find("label").text()).toBe(
    wrapper.vm.$props.item.title
  );
});

test("属性値の確認", () => {
  expect(textInput.props().placeholder).toBe(
    wrapper.vm.$props.item.placeholder
  );
});

👍 バリデーションチェックはしやすい

バリデーションチェックをやるときは、PHPUnitの場合、いわゆる DataProvider を使うことになるのがほとんどだと思いますが、PHPUnitだと、その実行概念を掴むまでが結構大変でした。というのも、PHPDocの中の @dataproviderDataProvider メソッド間を繋ぎ込む仕組みになっているのが「コメントなのに、処理に絡むの..?」と思っちゃうわけでして。

特徴的なものの一つだと思いますが、Pestの場合は1メソッドで簡潔しちゃうようになってます。

PHPUnitで書く

# PHPUnit

/**
 * @dataProvider phpUnitDataProvider
 */
public function test_バリデーションが動作する($item, $data, $expect)
{
    $request = new \App\Http\Requests\StoreRequest();
    $rules = $request->rules();
    $validator = Validator::make([$item => $data], [$item => $rules[$item]]);
    $result = $validator->passes();

    $this->assertEquals($expect, $result);
}

/**
 * データプロバイダ
 *
 * @dataProvider phpUnitDataProvider
 */
public function phpUnitDataProvider(): array
{
    $faker = Faker\Factory::create();

    return [
        'title_制限内入力' => ['title', \Illuminate\Support\Str::random(255), true],
        'title_空文字列入力' => ['title', 1234567890, true],
    ];
}

Pestで書く

# Pest

test('バリデーションが動作する', function ($name, $value) {
    $this->post(route('message.store'))->assertSuccessful()->assertValid();
})->with([
    ['message', str()->random(255)],
    ['message', 1234567890],
]);

👍 テスト結果がわかりやすい

これも嬉しいところです。実装者も毎回見る画面ですし、わかり良いUIなのは素敵なことです。
何件中、何件、みたいな簡素なものではなく、視覚的にもわかりやすい結果画面です。

Pestテストの実行結果

[laravel@localhost html]$ ./vendor/bin/pest tests/Feature/PestTest.php

   PASS  Tests\Feature\PestTest
  ✓ ID未指定 --> 404エラー
  ✓ 不正なID指定 --> 404エラー
  ✓ Pestでテストができる
  ✓ バリデーションが動作する

  Tests:  4 passed
  Time:   3.78s

[laravel@localhost html]$

👍 PHP Unitのアサーションも使える

これは前述の通りですが、PHPUnitやLaravelが提供しているアサーションが使えます。
なので、PHPUnitを前提としたテスト設計にも対応するので「Pestにしたからできなくなった」テストは無い印象です。


👎 結局、PHPUnit的感じ

で、前述のことと関わるのですが、結局のところPHPUnitでやっていたことの焼き直し感が否めません。

もちろん簡潔に書けるのが良いよね、というのは大前提ですけど、ロジックがPHPなので、テストケースもPHPUnitっぽくなっちゃうんですよね。特に Eloquent ORM Factory Faker JsonResource ... など、Laravelの特徴である処理を駆使していくと、見えない部分を根掘り葉掘りテスト検証をしていくことになるので、そうなるとPestのカプセルメソッドを使うより、その中で使っているPHPUnit系のメソッドに頼った方が合理的にかけたりなど。

テストしたいところまで突き詰めていくと、Pestじゃ無い感が増していきます・・

Pestでテストを書いてみる

// リクエスト
$response = $this->actingAs($this->user, 'web')->getJson(
    route('api.v1.hoge.index')
);

// ErrorException デバッグ
empty($response->exception) or dd($response->exception);

// 件数チェック
expect($response->getData()->response->data)->toHaveCount(1);

// ステータスチェック
$response->assertSuccessful();

// レスポンスデータ構造チェック
$response->assertJson(
    function (AssertableJson $json) use ($hoge) {
        $json->where('success', true)
            ->where('code', 200)
            ->where('status', 'OK')
            ->where('response.meta.total', 1)
            ->where('response.data.0', ['id' => $hoge->id]);
    }
);

👎 テスト実行速度とかはあんまり変わらない

ベースがPHPUnitなだけに、Pestを導入したからって速度が劇的に改善するかというとそうでもなかったです。
なので「大量のテストメソッドの処理改善をしたい」という要望には、完全には応えきれないものになります。


まぁ個人的な感覚なのと、2週間使った感想というところで、もしかすると良いところがどんどん出てくるかもしれないと淡い期待を持ちつつ。

Discussion