Laravelフォームリクエストのテスト


私はどのようにRolavelでフォームのリクエストをテストする必要があります決定するために苦労していた.具体的には、検証をテストしたいと思いました.私は迅速なGoogle検索を行いました、そして、それはそれをする複数の方法があるようです.つのブログ記事は、すべてのうち立っていたA guide to unit testing Laravel Form Requests in a different way そば@daaaan . 彼は、統合テストを書く代わりに、例えばit_fails_validation_without_title phpunitの@ dataprovidersの助けを借りて単体テストを使うべきです.
これは確かに私が試してみたかった何かだった、過去に私は、いくつかのテストメソッド、各テスト個々のリクエスト属性を要求を形成していたし、多くのコードの重複があった.
Danは以下を示唆します
<?php

namespace Tests\Feature\App\Http\Requests;

use App\Http\Requests\SaveProductRequest;
use Faker\Factory;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

class SaveProductRequestTest extends TestCase
{
    use RefreshDatabase;

    /** @var \App\Http\Requests\SaveProductRequest */
    private $rules;

    /** @var \Illuminate\Validation\Validator */
    private $validator;

    public function setUp(): void
    {
        parent::setUp();

        $this->validator = app()->get('validator');

        $this->rules = (new SaveProductRequest())->rules();
    }

    public function validationProvider()
    {
        /* WithFaker trait doesn't work in the dataProvider */
        $faker = Factory::create( Factory::DEFAULT_LOCALE);

        return [
            'request_should_fail_when_no_title_is_provided' => [
                'passed' => false,
                'data' => [
                    'price' => $faker->numberBetween(1, 50)
                ]
            ],
            'request_should_fail_when_no_price_is_provided' => [
                'passed' => false,
                'data' => [
                    'title' => $faker->word()
                ]
            ],
            'request_should_fail_when_title_has_more_than_50_characters' => [
                'passed' => false,
                'data' => [
                    'title' => $faker->paragraph()
                ]
            ],
            'request_should_pass_when_data_is_provided' => [
                'passed' => true,
                'data' => [
                    'title' => $faker->word(),
                    'price' => $faker->numberBetween(1, 50)
                ]
            ]
        ];
    }

    /**
     * @test
     * @dataProvider validationProvider
     * @param bool $shouldPass
     * @param array $mockedRequestData
     */
    public function validation_results_as_expected($shouldPass, $mockedRequestData)
    {
        $this->assertEquals(
            $shouldPass, 
            $this->validate($mockedRequestData)
        );
    }

    protected function validate($mockedRequestData)
    {
        return $this->validator
            ->make($mockedRequestData, $this->rules)
            ->passes();
    }
}

これはどうやって動くの?


テストメソッドで配列を提供しています.私たちはこのように多くのテストメソッドを持つことができます、そして、PHPUnitは残りの世話をします.追加@dataProvider 注釈validation_results_as_expected メソッドは、アサーションが実際に発生する場所です.各項目をループして呼び出しますvalidate テストケースでのメソッド.このメソッドは順番に呼び出します.validate on $this->validator で定義するsetUp .

問題


今、私は本当にこのアプローチが好きですが、私は実際に私のフォームの要求をテストしていないように感じた.にsetUp メソッドをフォームリクエストをインスタンス化しますが、検証規則を取得してからLaravelのバリデータに渡すだけです.これはおそらく、ほとんどの場合、単純なデータで動作しますが、私の場合、私はデータ操作をしていましたprepareForValidation フォームリクエストメソッド.私も、他のチェックをしていましたwithValidator メソッド.このテストアプローチでは、それらのメソッドは呼び出されません.また、私のフォームリクエストが期待通りに動作するかどうかはわかりません.

解決策?


バリデータを設定する代わりに、実際のフォームリクエストを設定する必要がありましたので、すぐに変更しましたvalidate 以下のようなテストケースでのメソッド
protected function validate($mockedRequestData)
{
    return (new CreateRedirectRequest())->...;
}
ちょっと待って.どのような方法を呼びますか.どうやってデータを渡すのですか?それから、私はIlluminate\Foundation\Http\FormRequest そして、それは広がるように見えますIlluminate\Http\Request , どちらが順番に広がるかSymfony\Component\HttpFoundation\Request . さて、フォームリクエストが実際にリクエストを拡張しているように見えます.
それで、私は容器からそれを分解すると思いました.validateメソッドを次のように変更しました.
protected function validate($mockedRequestData)
{
    app(CreateRedirectRequest::class);
}
私はテストを実行し、私はすぐにエラーで迎えられた.インマイwithValidator 私はこんな風にやっていました.
public function withValidator($validator)
{
    $validator->after(function ($validator) {
        if (! $this->parse($this->source)) {
            $validator->errors()->add('source', 'Invalid source');
        }
    });
}
エラーはArgument 1 passed to parse() must be of the type string, null given . どうやって動くのだろう.まだ検証を呼び出していませんがwithValidator メソッドは呼ばれています、そして、私はまだそれにデータさえ渡しませんでした.
検査したFormRequest もう一度クラスを利用しているようですValidatesWhenResolved インタフェースとValidatesWhenResolvedTrait 特性まあ、それは言うことです.この特性を使用しているオブジェクトがコンテナから解決されるとき、それは呼び出しますprepareForValidation , これは認証をチェックし、最終的に要求を正しく検証します.
FormRequestServiceProvider このコードを見ることができます.
 /**
 * Bootstrap the application services.
 *
 * @return void
 */
public function boot()
{
    $this->app->afterResolving(ValidatesWhenResolved::class, function ($resolved) {
        $resolved->validateResolved();
    });

    $this->app->resolving(FormRequest::class, function ($request, $app) {
        $request = FormRequest::createFrom($app['request'], $request);

        $request->setContainer($app)->setRedirector($app->make(Redirector::class));
    });
}
OK、それで、これは後で解決します、私は、ララベルもAを持っていますresolving コールバックので、解決する前にデータを渡すことができますか?変更しましたvalidate 次のような方法をもう一度行います.
protected function validate($mockedRequestData)
{
    $this->app->resolving(CreateRedirectRequest::class, function ($resolved) use ($mockedRequestData){
        $resolved->merge($mockedRequestData);
    });

    try {
        app(CreateRedirectRequest::class);

        return true;
    } catch (ValidationException $e) {
        return false;
    }
}

私はテストを実行し、彼らはすべて合格します.さて、このテストケースに新しいテストを追加したい場合は、新しい配列項目をデータに追加して渡す必要があるだけです.

では、なぜそれが動作しますか?


Laravelのコンテナがフォームリクエストを解決しているときに動作します
オブジェクトは、$mockedRequestData 配列を返します.
それが正常に解決するならば、我々はすべてのデータが有効であるということを知っていますIlluminate\Validation\ValidationException , どちらをキャッチしようとします.我々は真か偽かを返すvalidation_results_as_expected 次に、テストケースが通過するかどうかを比較します.また、'セットアップ'はもう必要ありません.

最終語


各リクエスト属性を個別にテストする複数のテストメソッドを記述している場合は、試してみてください.閉じるこの動画はお気に入りから削除されています.私は確かにこのアプローチを好む.

追伸


私はウェブ開発に新しくないが、これは私の最初のブログ記事です.あなたが将来的にこれのようなより多くのブログ柱を読みたいならば、私に続くか、コメントを残して、私はどんなフィードバックまたは質問でも歓迎します.