Laravel5.5LTS、AtoZ#6、テスト機能

タグ: Laravel5.5  

シリーズ第6弾です。今回はLaravelのテストを触ってみます。

テストの主題は、テストをどう行うか、名称の違いなど、設計やプログラミングと同様に主義や考え方が存在しています。

ユニットテスト

プログラミングの基本的な単位に対して実行するテストです。日本語では単体テストと呼ばれます。基本的なユニットとは、ファイルであったり、クラスであったりします。

ユニットテストでは、そしてそれ以降のテストでも、PHPの開発ではPHPUnitが使用されます。そのため、テスト実行にはPHPUnitの知識が必要です。

モックライブラリーとしてMockeryがサポートされています。

今回のシリーズではプログラミング的なことはまだ行っていませんし、ユニットテストとモックはLaravel特有な部分ではありません。そのため、ユニットテストは取り扱いません。テストのための設定などは用意済みですので、実行はプロジェクトルートでphpunitコマンドを起動するだけです。

ドキュメントで章(ページ)分けされているデータベースのテスト、HTTPテスト、ブラウザテストは、「ユニット、結合、機能、受け入れ」など、テストの段階を区別するものではありません。その名前が示すテストに関連した機能です。ですから、「HTTPテストは機能テスト」などのように言い切れません。あくまでも、Laravelが用意している機能単位の名前です。どのテスト機能をどのテスト段階で使用するかも、基本的に開発者次第であり、自由です。(Laravelフレームワークは、ユニットテストと機能テスト用のディレクトリを用意しています。)

データベース操作を含むテスト

ユニットテストにデータベース操作を含むべきか、含むべきでないかも長年のテーマです。

含んでしまえば、頻繁にユニットテストを行う開発環境では、オーバーヘッドにより実行が遅くなってしまいます。

もちろん、Laravelはどちらの意見も押し付けていません。データベースを含めたテストに便利な機能を提供しているだけです。

HTTPテスト

基本的にLaravelは、Webアプリを構築するためのフレームワークです。つまり、あるURLへブラウザなどからアクセスされ、Webサーバーが受け取ったリクエストを元にし、処理を行い、レスポンスを返すことで、ユーザーのブラウザに結果を返します。

つまり、特定のURLへのアクセスにより受け取るHTTPレスポンスが、正しいかどうかを確認すれば、Webアプリとしての機能がテストできるわけです。

まさにこのための機能をLaravelは用意しています。

前回、Laravelの組み込み認証を使ってみました。いろいろと遊んでみたかと思います。

唯一/senyou URLためのルート定義を追加しました。このページは/homeと同様に認証済みのユーザーしかアクセスできません。認証されていない状態でこのページヘアクセスすると、ログインページヘリダイレクトされ、ログインに成功すると/senyouのページヘ自動的にリダイレクトされます。

このように認証が必要なページヘ直接アクセスするのではなく、たとえばルートページの「ログイン」リンクからログインした場合は、ログイン完了後に/home URLへリダイレクトされます。これが、Laravelのデフォルトの認証動作の一部です。

ここでは、認証済み/未認証状態での/senyou URLへのアクセスをHTTPテストの機能を使用し確かめてみましょう。

開始する前に、現状でLaravelが用意したサンプルテストが、全部パスすることを確認しておきましょう。Homestead使用時は、もちろんHomestead環境で実行します。

phpunit

問題なくパスしたら、テストのクラスを生成します。

php artisan make:test AuthHttpTest

これにより、tests/Feature/AuthHttpTest.phpが生成されます。以下の内容に更新してください。テストメソッドが2つ含まれています。

<?php

namespace Tests\Feature;

use App\EloquentModels\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class SenyoPageHttpTest extends TestCase
{
    /**
     * 未認証状態での/senyouページヘのログインテスト
     */
    public function testRedirectToLoginWhenUnauthorized()
    {
        // 意図的に指定しなければ、未認証状態。
        $response = $this->get('senyou');

        // login URLヘリダイレクトされることを確認。
        $response->assertRedirect('login');
    }

    /**
     * 認証状態での/senyouページヘのログインテスト
     */
    public function testShowSenyouPageWhenAuthorized()
    {
        // ユーザインスタンスを作成する。
        // factoryを使用すると、ランダムデータが生成される。
        // データーがテストごとに変化してしまうため、この種のテストでは好ましくない。
        // そのため、自前でユーザーの情報を定義する。
        $user = (new User)->fill([
            'name' => 'Test Data',
            'email' => 'test@example.com',
        ]);

        // 作成したユーザーで認証済みにし、senyouページヘアクセス。
        $response = $this->actingAs($user)
            ->get('senyou');

        // アクセスに成功したことを確認
        $response->assertSuccessful();

        // ページに「専用ページ」と表示されていることを確認
        $response->assertSeeText('専用ページ');
    }
}

ブラウザテスト

たとえAPIアクセスをさばくことをメインとしてシステムが組まれていても、そのシステムがブラウザから操作される前提であれば、ブラウザ上での操作シナリオ通りに使用可能であるかを確認したいのは当然です。

つまり、ログインボタンをクリックしたら、ログインフォームが表示され、正しい認証情報を入力し送信すれば、ログイン後に表示したいページがきちんと表示されることを確認したいわけです。

こうしたブラウザでの自動操作と確認作業をサポートするツールとしては、Seleniumが有名です。

Laravel Duskはこうした画面操作の自動化と確認作業をLaravelの環境で簡単に実現するためのパッケージです。

ドキュメントに従い、インストールしましょう。Homestead環境を使用している場合は、Homestead環境にインストールしましょう。

composer require --dev laravel/dusk

もし、以下のようなエラーが発生した場合は、もう一度コマンドを繰り返し、インストールし直してください。

Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover


  [ErrorException]
  array_merge(): Argument #2 is not an array


Script @php artisan package:discover handling the post-autoload-dump event returned with error code 1

Removal failed, reverting ./composer.json to its original content.

依存パッケージをインストールしたあとに、クラスのオートロードに必要なファイルを生成し、その後に実行されるLaravelのスクリプトでエラーが起きることがあります。

そのため、composer dump-autoload実行時も、同じエラーが発生することがあります。

不具合がLaravel側に存在するのか、Composer側にあるのか分かりません。しかし、対処方法としては、正常終了するまで繰り返せば大丈夫です。

パッケージをインストールしたら、Duskの環境を整えます。

php artisan dusk:install

インストールできたがどうかを確認するため、Duskによるテストを実行しましょう。

php artisan dusk

一つのテストが実行され、パスします。次に、テストを作成します。

php artisan dusk:make SenyouPageBrowserTest

tests/Browser/SenyouPageBrowserTest.phpにテストが作成されます。以下の内容に変更してください。

<?php

namespace Tests\Browser;

use App\EloquentModels\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;

class SenyouPageBrowserTest extends DuskTestCase
{
    public function testWhenAuthorized()
    {
        $this->browse(function (Browser $browser) {
            $browser
                // 最初の登録ユーザーとしてログイン状態にする
                ->loginAs(User::find(1))
                // senyou URLへアクセス
                ->visit('/senyou')
                // senyou URLへ移行したことを確認
                ->assertPathIs('/senyou')
                // 「専用ページ」が表示されていることを確認
                ->assertSee('専用ページ')
                // 次のテストのために、ログアウト状態へ戻す
                // ログアウトリンクがないため、まずルートページヘ戻る
                ->visit('/')
                // ログアウトリンクをクリック
                ->clicklink('Logout');
        });
    }

    public function testWhenUnauthorized()
    {
        $this->browse(function (Browser $browser) {
            $browser
                // senyou URLへアクセス
                ->visit('/senyou')
                // login URLへ移行したことを確認
                ->assertPathIs('/login')
                // メールアドレス入力
                ->type('email', 'hirokws@gmail.com')
                // パスワード入力
                ->type('password', 'hirokws')
                // loginボタンクリック
                ->click('.panel-body button')
                // senyou URLへ以降したことを確認
                ->assertPathIs('/senyou')
                // 「専用ページ」が表示されていることを確認
                ->assertSee('専用ページ');
        });
    }
}

操作とチェックを自由に組み合わせられます。ドキュメントに記載されている通り、同じテストの実行中はログイン状態が維持されるため、ログイン次のテストの最後にログアウトしています。

実際に試してもらえばわかりますが、結構オーバーヘッドがかかります。

利用する前に、アプリケーションのURLをapp.php設定ファイルに指定する必要がありますが、Homestead環境で動作しているサイトではhttpsを指定すると、うまくテストできません。これはSSLが自家認証ですから安全ではないため、ブラウザ(ChromeDriver)がアクセスしないからです。

通常のChromeで開くと、安全ではないHTTPSサイトでは、危険を承知でアクセスするか確認されます。一度確認すると、以降は通常通りアクセス可能になります。(URLバーには、安全でないとアイコンで表示されます。)こうした情報は設定環境であるプロファイルに保存されます。

ChromeDriverでも通常のChromeと同様に、プロファイルを指定できますので、工夫すれば通常のChromeで一度httpsとしてアクセスしておき、そのプロファイルをHomestead環境にコピーし、テスト実行時に指定するようにできれば、httpsでも利用可能だと思います。ただし、実際に試したわけではありません。

この例では、Homesteadで動作させる方法を取りましたが、ブラウザによるサーバーアクセスを含めたテストなのですから、ホスト側でDuskによるテストを実行しても、もちろんかまいません。ただし、ホスト側で動作させる準備が簡単だとは言えません。(難しいとも言えません。OSや環境に依ります。)

ユニットテストの自動実行

ファイルの変更を監視し、特に自分が担当する開発分の単体テストを自動実行できるようにするのは、常套手段です。IDEには組み込まれています。

ところが、Homesteadを使用している場合、Homestead環境が開発環境なのですから、PHPなどのバージョンを合わせるため、Homestead内で実行することになります。すると、通常ホストマシン上で動作しているIDEなどからはHomestead環境でPHPUnitを直接起動するのは、手間がかかります。

Laravel Mixではファイル監視の結果によりPHPUnitを実行できません。要望はたびたび出ていますが、開発者のJeffry Way氏が、「目的が異なる」と拒否しています。つまり、Mixはアセットコンパイルのためのツールであり、テスト実行のためのものではないからです。

Mixで使用しているwebpackを利用する手もありますが、そもそも、PHPのテストですから、PHPの環境に監視実行ツールは当然用意されています。わざわざ、nodeやらJavaScriptやらを触る必要はありません。解決策はシンプルにしましょう。

spatie/phpunit-watcherパッケージです。まず、インストールしましょう。全プロジェクト弟子用意できるようにグローバルなパッケージとしてインストールします。

composer global require spatie/phpunit-watcher

これで、~/.composer/vendor/bin/phpunit-watcher watchで実行可能ですが、毎回この長いコマンドを叩くのはつらいですので、エイリアスとして登録しましょう。

Homesteadディレクトリー下のaliasesファイルに一行追加します。

alias watch='~/.composer/vendor/bin/phpunit-watcher watch'

Homestead環境が動作中であればvagrant provision、起動していなければvagrant up --provisionを実行します。

これにより、Laravelプロジェクトのルートで、watchを実行するとファイル監視状態になり、自動テストが行われます。監視中は、コンソールに制御が返ってきません。qキーを押すと実行が停止します。

ファイルの変更が無くてもEnterキーを押せば、テストが実行されます。コマンドは表示されます。

デフォルトでも動作しますが、よりLaravelに合わせて動作させるために、設定ファイルを使用しましょう。Laravelのルートプロジェクトに、.phpunit-watcher.ymlファイルを以下の内容で作成します。

watch:
  directories:
    - app
    - tests
  fileMask: '*.php'
notifications:
  passingTests: false
  failingTests: false

この設定ではapptestsディレクトリー下のみ対象です。ユニットテストですので、これで支障はないと思いますが、他のディレクトリーも含めたい場合は、watch項目へ追加してください。

便利ですが、欠点は多少CPUを喰うことでしょうか。手元の環境では1CPUの25%を使用しています。これが気になる方は、inotify-toolを使用する方法を試してみると良いでしょう。古いですが、当サイトでも過去にCIに関する記事として紹介したことがあります。

inotify-toolはHomesteadのバージョン4環境にあらかじめ含まれています。そのため、インストールする必要はありません。将来のバージョンで含まれなくなったら、自前でインストールしてください。