web-dev-qa-db-ja.com

Laravelテストケースでhttpリクエストをシミュレートし、ルートパラメータを解析します

特定のクラスをテストするための単体テストを作成しようとしています。テストするクラスをインスタンス化するには、app()->make()を使用します。したがって、実際にはHTTP要求は必要ありません。

ただし、テストされた関数の一部はルーティングパラメータからの情報を必要とするため、関数は呼び出しを行います。 request()->route()->parameter('info')、そしてこれは例外をスローします:

Nullのメンバー関数parameter()への呼び出し。

私はたくさん遊んで、次のようなことを試しました:

_request()->attributes = new \Symfony\Component\HttpFoundation\ParameterBag(['info' => 5]);  

request()->route(['info' => 5]);  

request()->initialize([], [], ['info' => 5], [], [], [], null);
_

しかし、どれもうまくいきませんでした...

ルーターを手動で初期化し、いくつかのルーティングパラメータをルーターに送るにはどうすればよいですか?または単にrequest()->route()->parameter()を使用可能にしますか?

更新

@Loek:あなたは私を理解していませんでした。基本的に、私はやっています:

_class SomeTest extends TestCase
{
    public function test_info()
    {
        $info = request()->route()->parameter('info');
        $this->assertEquals($info, 'hello_world');
    }
}
_

「リクエスト」は含まれません。 request()->route()->parameter()呼び出しは、実際には実際のコードのサービスプロバイダーにあります。このテストケースは、そのサービスプロバイダーをテストするために特に使用されます。そのプロバイダーのメソッドからの戻り値を出力するルートはありません。

13
Frederick Zhang

実際にディスパッチせずにリクエストをシミュレートする必要があると思います。シミュレートされたリクエストが用意できたら、それを調べてパラメーター値を調べ、テストケースを開発します。

これを行うには文書化されていない方法があります。あなたは驚かれることでしょう!

問題

すでにご存じのとおり、Laravelの _Illuminate\Http\Request_ クラスは _Symfony\Component\HttpFoundation\Request_ に基づいています。上流のクラスでは、setRequestUri()の方法で手動でリクエストURIを設定することはできません。実際のリクエストヘッダーに基づいて計算されます。他の方法はありません。

はい、おしゃべりで十分です。リクエストをシミュレートしてみましょう:

_<?php

use Illuminate\Http\Request;

class ExampleTest extends TestCase
{
    public function testBasicExample()
    {
        $request = new Request([], [], ['info' => 5]);

        dd($request->route()->parameter('info'));
    }
}
_

あなたがあなた自身に言及したように、あなたは以下を得るでしょう:

エラー:nullのメンバー関数parameter()への呼び出し

Routeが必要です

何故ですか? route()nullを返す理由

その実装 と、それに対応するメソッドの実装を見てください。 getRouteResolver()getRouteResolver()メソッドは空のクロージャを返し、次にroute()がそれを呼び出すため、_$route_変数はnullになります。次に、それが返され、エラーが発生します。

実際のHTTPリクエストコンテキストでは、 Laravelはルートリゾルバーを設定します なので、このようなエラーは発生しません。リクエストをシミュレートしているので、自分で設定する必要があります。方法を見てみましょう。

_<?php

use Illuminate\Http\Request;
use Illuminate\Routing\Route;

class ExampleTest extends TestCase
{
    public function testBasicExample()
    {
        $request = new Request([], [], ['info' => 5]);

        $request->setRouteResolver(function () use ($request) {
            return (new Route('GET', 'testing/{info}', []))->bind($request);
        });

        dd($request->route()->parameter('info'));
    }
}
_

Laravel独自のRouteクラス からRouteCollectionsを作成する別の例を参照してください。

空のパラメーターバッグ

したがって、リクエストオブジェクトがバインドされたルートが実際にあるため、このエラーは発生しません。しかし、まだ機能しません。この時点でphpunitを実行すると、nullが表示されます。 dd($request->route())を実行すると、infoパラメータ名が設定されていても、そのparameters配列が空であることがわかります。

_Illuminate\Routing\Route {#250
  #uri: "testing/{info}"
  #methods: array:2 [
    0 => "GET"
    1 => "HEAD"
  ]
  #action: array:1 [
    "uses" => null
  ]
  #controller: null
  #defaults: []
  #wheres: []
  #parameters: [] <===================== HERE
  #parameterNames: array:1 [
    0 => "info"
  ]
  #compiled: Symfony\Component\Routing\CompiledRoute {#252
    -variables: array:1 [
      0 => "info"
    ]
    -tokens: array:2 [
      0 => array:4 [
        0 => "variable"
        1 => "/"
        2 => "[^/]++"
        3 => "info"
      ]
      1 => array:2 [
        0 => "text"
        1 => "/testing"
      ]
    ]
    -staticPrefix: "/testing"
    -regex: "#^/testing/(?P<info>[^/]++)$#s"
    -pathVariables: array:1 [
      0 => "info"
    ]
    -hostVariables: []
    -hostRegex: null
    -hostTokens: []
  }
  #router: null
  #container: null
}
_

したがって、その_['info' => 5]_をRequestコンストラクタに渡しても、何の影響もありません。 Routeクラスを見て、その _$parameters_プロパティ がどのように入力されるかを見てみましょう。

ルートに リクエストをバインド オブジェクトすると、次に_$parameters_プロパティが bindParameters() メソッドへの後続の呼び出しによって入力されます。 bindPathParameters() を呼び出して、パス固有のパラメーターを特定します(この場合、Hostパラメーターはありません)。

このメソッドは、リクエストのデコードされたパスを正規表現 Symfonyの_Symfony\Component\Routing\CompiledRoute_ と照合し(上記のダンプでその正規表現も確認できます)、パスパラメータである一致を返します。パスがパターンに一致しない場合は空になります(これは私たちの場合です)。

_/**
 * Get the parameter matches for the path portion of the URI.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return array
 */
protected function bindPathParameters(Request $request)
{
    preg_match($this->compiled->getRegex(), '/'.$request->decodedPath(), $matches);
    return $matches;
}
_

問題は、実際のリクエストがない場合、その$request->decodedPath()がパターンに一致しない_/_を返すことです。したがって、パラメータバッグは何があっても空になります。

リクエストURIのスプーフィング

RequestクラスでdecodedPath()メソッドを実行する場合、いくつかのメソッドを深く調べ、最終的に prepareRequestUri()から値を返します。 of _Symfony\Component\HttpFoundation\Request_。そこで、まさにその方法で、あなたはあなたの質問に対する答えを見つけるでしょう。

これは、一連のHTTPヘッダーを調べて、リクエストURIを把握しています。最初に_X_ORIGINAL_URL_をチェックし、次に_X_REWRITE_URL_、次に他のいくつかをチェックし、最後に_REQUEST_URI_ヘッダーをチェックします。これらのヘッダーのいずれかを実際にリクエストURIにspoof設定して、httpの最小シミュレーションを実現できます。リクエスト。どれどれ。

_<?php

use Illuminate\Http\Request;
use Illuminate\Routing\Route;

class ExampleTest extends TestCase
{
    public function testBasicExample()
    {
        $request = new Request([], [], [], [], [], ['REQUEST_URI' => 'testing/5']);

        $request->setRouteResolver(function () use ($request) {
            return (new Route('GET', 'testing/{info}', []))->bind($request);
        });

        dd($request->route()->parameter('info'));
    }
}
_

驚いたことに、これは_5_を出力します。 infoパラメータの値。

掃除

機能をヘルパーsimulateRequest()メソッド、またはテストケース全体で使用できるSimulatesRequestsトレイトに抽出することができます。

あざける

上記のアプローチのようにリクエストURIを偽装することが絶対に不可能であったとしても、リクエストクラスを部分的にモックして、予想されるリクエストURIを設定できます。以下に沿ったもの:

_<?php

use Illuminate\Http\Request;
use Illuminate\Routing\Route;

class ExampleTest extends TestCase
{

    public function testBasicExample()
    {
        $requestMock = Mockery::mock(Request::class)
            ->makePartial()
            ->shouldReceive('path')
            ->once()
            ->andReturn('testing/5');

        app()->instance('request', $requestMock->getMock());

        $request = request();

        $request->setRouteResolver(function () use ($request) {
            return (new Route('GET', 'testing/{info}', []))->bind($request);
        });

        dd($request->route()->parameter('info'));
    }
}
_

これは_5_も出力します。

19
sepehr

routeはクロージャとして実装されているため、parameter('info')を明示的に呼び出さなくても、ルート内のルートパラメータに直接アクセスできます。これら2つの呼び出しは同じ結果を返します。

_$info = $request->route()->parameter('info');
$info = $request->route('info');
_

2番目の方法は、「info」パラメーターのモックを非常に簡単にします。

_$request = $this->createMock(Request::class);
$request->expects($this->once())->method('route')->willReturn('HelloWorld');
$info = $request->route('info');
$this->assertEquals($info, 'HelloWorld');
_

もちろん、このメソッドをテストで利用するには、request()メソッドを通じてLaravelグローバルリクエストオブジェクトを使用する代わりに、テスト中のクラスにRequestオブジェクトを注入する必要があります。

0
T30