LaravelではFeatureテストが簡単に行えるように、便利なテスト用メソッドが用意されています。今回はそちらを使っていてハマってしまった意図せぬ挙動について紹介します。

HTTPテスト

例えば特定のURLにリクエストを投げ、正しいレスポンスが返されるか?などのFeatureテストをする際に、Laravelでは以下のようなテストが書けます。

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_a_basic_request()
    {
        $response = $this->get('/'); // HTTPリクエストをシミュレート

        $response->assertStatus(200); // レスポンスのステータスコードテスト

        $response->assertSee('Laravel'); // レスポンスに指定した文字列が含まれているかテスト
    }
}

上のテストでは/にGETリクエストを投げ、リクエストが正常に処理されたかステータスコード200をチェック、そして返されたレスポンスにLaravelという文字列が含まれているかをテストします。

インストール直後の状態では/にアクセスすると以下のページを返しますので、テストは成功するはずです。

URLに日本語が含まれている場合の挙動

さて、ここからが本題です。私がハマったのはURLに日本語が含まれている場合のテストです。
他の開発者のwindows環境ではパスするテストが、私のmacOS環境では必ず失敗してしまう、
という状況が発生しました。

どんなテストかを説明する為に例を用意しました。以下のようなページがあるとします。

入力した検索ワードをそのまま出力するシンプルなページです。

viewはこんな感じ。

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Search</title>
</head>
<body>
    <form action="/search" method="get">
        <input type="text" name="word" value="">
        <input type="submit" value="検索">
    </form>

    <h1>{{ $word }}</h1>
</body>
</html>

routeはこんな感じ。

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/search', function(Request $request) {
    $word = $request->get('word', '');

    return view('search', compact('word'));
});

前項のExampleTestをこのページに関するテストに書き換えてみましょう。

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ExampleTest extends TestCase
{

    public function test_search_page()
    {
        $response = $this->get('/search?word=ほげ');

        $response->assertStatus(200);

        $response->assertSee('ほげ');
    }
}

“ほげ”というワードで検索した場合をシミュレートする為、/search?word=ほげにリクエストを投げ、レスポンスに文字列「ほげ」が含まれているかテストしています。

テストを実行してみましょう。

vendor/bin/phpunit --filter ExampleTest

PHPUnit 9.5.10 by Sebastian Bergmann and contributors.

.F                                                                  2 / 2 (100%)

Time: 00:00.138, Memory: 20.00 MB

There was 1 failure:

1) Tests\Feature\ExampleTest::test_search_page
Failed asserting that '<!DOCTYPE html>\n

~~省略~~

    <h1></h1>\n
</body>\n
</html>' contains "ほげ".

レスポンスに「ほげ」が含まれていない、との事でテストが失敗してしまいました。確かに、レスポンス内で検索ワードが表示される箇所が

    <h1></h1>\n

となっています。

ちなみに、検索文字列を「hoge」にした場合はパスします。

~~~
    public function test_search_page()
    {
        $response = $this->get('/search?word=hoge'); // hoge に変更

        $response->assertStatus(200);

        $response->assertSee('hoge'); // hoge に変更
    }
~~~
vendor/bin/phpunit --filter ExampleTest
PHPUnit 9.5.10 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 00:00.138, Memory: 20.00 MB

OK (2 tests, 3 assertions)

なぜでしょう?

parse_url()のバグ

$this->get()のソースを解析してみると、
vendor/symfony/http-foundation/Request.php 355行目parse_url()にてURLからパースしたクエリ文字列が文字化けしていました。

以下はtinkerで同じコードを実行した際の結果です。

parse_url('http://127.0.0.1:8000/search?word=ほげ');
=> [
     "scheme" => "http",
     "host" => "127.0.0.1",
     "port" => 8000,
     "path" => "/search",
     "query" => b"word=ã_»ã__",
   ]

文字化けしていますね。ググってみるとGitHubで以下のIssueを見つけました。

UTF-8 URIs are broken by parse_url when locale is set

parse_url()platform(macなど)やlocaleの設定によってパースした文字列が壊れる場合があるとのこと。2010年から報告されているこちらのPHPのバグのようです。

urlencode()で解決

回避策として予め日本語部分をurlencode()でエンコードすれば良さそうです。
ExampleTestを以下のように修正しました。

~~~
    public function test_search_page()
    {
        $word = urlencode('ほげ');  // 予めURLエンコード

        $response = $this->get('/search?word='.$word);

        $response->assertStatus(200);

        $response->assertSee('ほげ');
    }
~~~

テストが通るようになりました。

vendor/bin/phpunit --filter ExampleTest
PHPUnit 9.5.10 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 00:00.136, Memory: 20.00 MB

OK (2 tests, 3 assertions)

まとめ

HTTPテストで日本語を含むURLにリクエストを投げる際は、日本語部分をURLエンコードするのが良さそうです。

By hikaru