インフィニットループ 技術ブログ

2017年05月12日 (金)

著者 : m-yamagishi

Laravel 5.4 で手軽にテストを書こう!

こんにちは。最近はお茶が趣味なんです、というとまるでお上品な人のように思えてくる YamaYuski です。
さて、巷ではどうやら「ユニットテスト」なるものが(数年前から)流行しているそうですね。
当技術ブログでも、

とテストに関する記事があります。
弊社も、最近のプロジェクトではテストを書く前提でPRが飛び交っています。
老舗の(PHP5.2等の時代からある)いわゆるレガシーコードを保守するプロジェクトでも、新規にテストコードを追加し続けている所が多いです。
その中で今回は、PHPのWebアプリケーションフレームワーク Laravel におけるテスト手法をご紹介します。充実したサポートのおかげで、手軽にテストを書けるんです!

Laravel のテストサポートは?

公式のドキュメントはこちらです
現在、 Laravel 5.4 では PHPUnit 5.7 をベースフレームワークとしてテストを行うことが出来ます。
もちろん、 BehatCodeception など、他のテストフレームワークを利用して
Laravel アプリケーションをテストすることも可能ですが、今回は別のライブラリやフレームワークは入れず、素のままの Laravel テストのみ踏まえてまとめています。
Laravel アプリケーションを作成した時点で、既に vendor/bin/phpunit とコマンドを打てばすぐにテストが実行出来るでしょう。

$ vendor/bin/phpunit
PHPUnit 5.7.19 by Sebastian Bergmann and contributors.
..                                                                  2 / 2 (100%)
Time: 872 ms, Memory: 6.00MB
OK (2 tests, 2 assertions)

そう。 Laravel アプリケーションでは、事前の設定なしですぐにテストを書くことが出来るわけです。では早速書き始めましょう。

テストの種類

Laravel では、

  • アプリケーションテスト
  • ブラウザテスト(※Laravel 5.4以上)
  • 普通のユニットテスト

を行うことが出来ます。
普通のユニットテストは言わずもがなですが、 Laravel では追加で アプリケーションテスト 及び ブラウザテスト を手軽に行える仕組みがそろっています。
一つ一つみていきましょう。

アプリケーションテスト

アプリケーションテスト(HTTPテスト、Featureテストとも)は、テストケースごとに構築された Laravel Application に対して仮想のリクエストを投げ、レスポンスとして返ってきたレスポンスコードやjsonが正しいかどうかチェックする、APIレベルのテストです。

<?php
namespace Tests\Feature;
class BasicTest extends \Tests\TestCase
{
    /**
     * 通常のGETリクエストが正しく処理されるか
     */
    public function testBasicTest()
    {
        $response = $this->get('/');
        $response->assertStatus(200);
    }
    /**
     * ログインした状態でリクエストが正しく処理されるか
     */
    public function testLoggedIn()
    {
        $user = factory(\App\User::class)->create();
        $response = $this->actingAs($user)
                         ->get('/');
        $response->assertStatus(200);
    }
}

factory に関しては後述しますが、このように記述するだけで仮想のHTTPリクエストを生成し、レスポンスを取得してくれます。
HTTPアクセスで通過するコードの基礎的なエラー(use漏れやreturn忘れなど)のチェックに有効です。

<?php
namespace Tests\Feature;
class JsonTest extends \Tests\TestCase
{
    public function testJson()
    {
        $response = $this->json('POST', '/user', ['name' => 'Sally']);
        $response
            ->assertStatus(200)
            ->assertJson([
                'created' => true,
            ]);
    }
}

また、jsonAPIのテストにおいて真価を発揮します。

  1. factory などで必要なデータの用意
  2. リクエストの送信
  3. レスポンスのアサーション

の三点にテストコードを集中させることが出来るので、少ないコード量で適切にテストコードを記述することが出来ます。
同時に、CookieやSession情報が正しく設定されているかのアサーションも行うことが出来ますよ。

ブラウザテスト

これが今回の目玉商品(機能)です!

Laravel 5.4 より、 Laravel Dusk という新しいテストライブラリが利用できるようになりました。
このライブラリは ChromeDriver というブラウザを自動操作出来る機能を持っていて、自動でブラウザを(別プロセスで)立ち上げてHTTPアクセスを行ったり、DOMやjavascriptを操作したりします。
準備は少し複雑ですが、一度環境が整えばすぐにブラウザテストが書けるようになるでしょう。

# 別リポジトリになっているので composer require
$ composer require "laravel/dusk" --dev
# 初期ディレクトリ・ファイルの生成
$ php artisan dusk:install
// app/Providers/AppServiceProvider.php に追記
/**
 * Register any application services.
 *
 * @return void
 */
public function register()
{
    if ($this->app->environment('local', 'testing')) {
        $this->app->register(\Laravel\Dusk\DuskServiceProvider::class);
    }
}

これで初期化は完了です。
実際のテスト実行のためには、事前に普通にブラウザからアクセス出来るアプリケーション環境(php artisan serveでもOK)を用意しておく必要があります。
実際に別プロセスでブラウザを立ち上げてからアクセスするので、必須ですね。
そして、そのアプリケーション環境に繋がるよう .env の APP_URL を設定しておきます。
で、実際にテストを /tests/Browser/ 内に書いていきます。

<?php
namespace Tests\Browser;
class ExampleTest extends \Tests\DuskTestCase
{
    /**
     * ログインしてみる
     */
    public function testBasicExample()
    {
        $user = factory(\App\User::class)->create([
            'email' => 'taylor@laravel.com',
        ]);
        $this->browse(function ($browser) use ($user) {
            $browser->visit('/login')
                    ->type('email', $user->email)
                    ->type('password', 'secret')
                    ->press('Login')
                    ->assertPathIs('/home');
        });
    }
}

Laravel 5.3 まで、上記 アプリケーションテスト で行っていたようなDOM操作(typepress)の記述で、テストを書くことが出来ます。
実際に実行するには、 vendor/bin/phpunit の代わりに php artisan dusk を実行します。

$ php artisan dusk
PHPUnit 5.7.19 by Sebastian Bergmann and contributors.
.                                                                   1 / 1 (100%)
Time: 9.37 seconds, Memory: 4.00MB
OK (1 test, 1 assertion)

このコマンドを叩くと、Windowsなどで動かした場合は自動でChromeブラウザが立ち上がって、指定された入力を勝手に行ってくれます。

なんかカッコイイ。
※このDusk Testを行うためには、上記のようにテスト環境にChromium/Google Chromeブラウザ自体がインストールされている必要があります。
Linuxでも仮想ディスプレイを作成することで対応可能ですが、今回はその手法については省略します。

また、以前の Laravel では確認出来なかった「キーボードの1キー単位の入力」「マウスの操作」「javascriptの操作」を行うことが出来ます。より現実のユーザテストに近い自動テストを実現することが出来ますね。凄い!
ブラウザの操作自体は facebook/php-webdriver を利用しているので、こちらのドキュメントを参考にすると、より的確な操作とアサーションが行えるのではないかと思います。
この手法は、開発者が手軽に実際のブラウザの自動テストが書ける手法ということで、皆さんに強くお勧めしたい所です!
(筆者はまだ5.3環境なので使えていない…。)

普通のユニットテスト

普通に PHPUnit を使って、普通に特定クラスの単機能テストを行えます。
普通のユニットテストの作り方については今回は省略しますが、後述のサポート機能を併用してより簡単に書けるのではないかと思います。


ユニットテストを行うにあたって、

  • モック化がつらい。そもそもモックってなんだ
  • データベース処理をすべてモックするのはつらい
  • テストに必要なデータの準備が面倒

など、共通の問題点がよく聞かれます。
Laravel ではそういった共通課題を解決出来る機能を有しているので、より簡単に、より素早くテストコードを記述することが出来ます。

モック化

ユニットテストで、テスト対象のメソッドが依存している外部の要素(外部クラスやDB)は「モック化 (条件等で色々名前が変わるのですが、ここでは単にモック化と言います)」を行うべきとされています。
外部依存のバグでテストが失敗することを防ぐためであるとか、外部依存のための初期化処理(データの挿入など)を行うと、ユニットテスト自体のコード量が増え、視認性が悪くなってしまうためですね。
そこで、テスト対象のメソッド以外の外部クラスやDB操作などをモック化して、「こういう引数でこのメソッドを呼んだら、こういう戻り値を返す」という宣言を事前に行い、宣言通りに操作が行われたかどうかをアサーションするモックオブジェクトを利用してテストを行います。
モック化は PHPUnit 自体の機能に含まれています。

<?php
class ExampleTest extends TestCase
{
    public function testObserversAreUpdated()
    {
        // Observer クラスのモックを作成します。
        // update() メソッドのみのモックです。
        $observer = $this->getMockBuilder(Observer::class)
                         ->setMethods(['update'])
                         ->getMock();
        // update() メソッドが一度だけコールされ、その際の
        // パラメータは文字列 'something' となる、
        // ということを期待しています。
        $observer->expects($this->once())
                 ->method('update')
                 ->with($this->equalTo('something'));
        // Subject オブジェクトを作成し、Observer オブジェクトの
        // モックをアタッチします。
        $subject = new Subject('My subject');
        $subject->attach($observer);
        // $subject オブジェクトの doSomething() メソッドをコールします。
        // これは、Observer オブジェクトのモックの update() メソッドを、
        // 文字列 'something' を引数としてコールすることを期待されています。
        $subject->doSomething();
    }
}

このような形でモックオブジェクトを作成し、これをテスト対象のクラスに渡してテストを行います。
また、静的メソッドを利用する Facade は Mockery というモッキングライブラリを利用してモック化することが出来ます。

<?php
class UserControllerTest extends TestCase
{
    public function testGetIndex()
    {
        Cache::shouldReceive('get')
                    ->once()
                    ->with('key')
                    ->andReturn('value');
        $response = $this->get('/users');
        // ...
    }
}

その他にも、 Laravel の各種機能はテストしやすいようモックの準備が出来ています。詳しくは Mocking – Laravel 5.4 をご覧ください。
…このモックオブジェクト宣言、ちょっと面倒ですね。もっと分かりやすくて使いやすいモック化は出来ないものでしょうか。
その答えの一つに Prophecy があります。

Prophecy

Prophecy は、ブラウザテストに続く今回イチオシのライブラリです!
これは最近のPHPUnitに同梱されるようになった、新しめのモッキングライブラリです。実際のコードを見てみましょう。

<?php
class ExampleTest extends TestCase
{
    /**
     * PHPUnit のモックを使った実装
     */
    public function testObserversAreUpdatedWithPhpunitMock()
    {
        // モックの生成
        $observer = $this->getMockBuilder(Observer::class)
                         ->setMethods(['update'])
                         ->getMock();
        // モックの宣言
        $observer->expects($this->once())
                 ->method('update')
                 ->with($this->equalTo('something'));
        // 実行
        $subject = new Subject('My subject');
        $subject->attach($observer);
        $subject->doSomething();
    }
    /**
     * Prophecy を使った実装
     */
    public function testObserversAreUpdatedWithProphecy()
    {
        // モックの生成
        $observer = $this->prophesize(Observer::class);
        // モックの宣言
        $observer->update('something')
                 ->shouldBeCalled();
        // 実行
        $subject = new Subject('My subject');
        $subject->attach($observer->reveal());
        $subject->doSomething();
    }
}

さて、上記のコード二つを比べて、どちらが良いと感じますか?私は下の Prophecy がコード量も少なく、直感的な宣言を行えるので分かりやすいと思っています。
ネックになるのは $subject->attach($observer->reveal()); と、インスタンスを渡す時のメソッドが増えている点ですね。
これ、毎回忘れて謎のエラーが出ます。
それを除けば、シンプルな宣言でモック化を行えるので、是非オススメしたい所。

データベースを利用したテスト

ロジックの中でDBの操作は大きな比重を持っています。それらのDBの操作を全てモック化するのは非常に大変です。
そこで、 Laravel では、必要なDBのレコードを簡単に生成し、そのレコードを利用したテストを行い、正しくレコードが更新されたかをチェックする仕組みが用意されています。

手軽なレコード生成

Laravel には DatabaseFactory というものが存在します。
これは、事前に各 Model の情報を生成しておくことで、テスト時に簡単に Model を
new/insert 出来る仕組みです。

$factory->define(App\User::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'password' => bcrypt('secret'),
        'remember_token' => str_random(10),
    ];
});

Factory ファイルにこのように定義すると、テスト時に

<?php
namespace Tests\Feature;
class BasicTest extends \Tests\TestCase
{
    /**
     * ログインした状態でリクエストが正しく処理されるか
     */
    public function testLoggedIn()
    {
        // Model の生成(+ insert)
        $user = factory(\App\User::class)->create();
        $response = $this->actingAs($user)
                         ->get('/');
        $response->assertStatus(200);
    }
}

factory グローバル関数を呼ぶだけで、そのモデルを生成出来ます。
->create(); とすることで INSERT も同時に行えます。
また、データべースの更新が行われたかのテストを行うためのメソッドとして

    $this->assertDatabaseHas('users', [
        'email' => 'sally@example.com'
    ]);

といったものがあります。これで、

  • 動作用のデータの準備
  • 動作後のデータ変化のアサーション

を手軽に行うことが出来ます。

Faker によるダミーデータ生成

Factory の定義時に、 Faker\Generator $faker というインスタンスが渡されてきます。
これは fzaninotto/Faker と呼ばれるテスト用のダミーデータ生成ライブラリです。
通常であれば "foo@example.com" だとか、 "Taro Yamada" だとかいったダミーデータを都度生成しますが、この Faker を利用すると、よくテストで使われる文字列や数値を、自動で生成してくれます。
住所や電話番号、名前、会社名などなど、多彩なダミーデータを、各国に合わせた形でランダムに取得することが可能です。

トランザクションでテストDBを汚さない

テストケースに \Illuminate\Foundation\Testing\DatabaseTransactions というtraitを加えることで、テストの前後でトランザクションを張って、テスト時のレコード挿入や更新をそのテストだけとすることが可能です。
Laravel のトランザクションは複数回行えるようサポートされているので、テスト内でコミットを行っても、実際にコミットされることはありません。
これで、テスト用のDBを汚すことなくテストを行うことが出来ます(AUTO_INCREMENTの値など、一部は影響を受けてしまいます)。

まとめ

  • Laravel なら結合テストも単体テストも簡単にかけるぜ
  • DBを使ったテストもデータの用意も簡単だぜ
  • モックも Prophecy で直感的に行えるぜ

と、 Laravel アプリケーションは素晴らしいテスタブル環境にあると言えます。皆さんも Laravel を使って何かアプリケーションを組む時は、是非これらのテストを体験してみてくださいね!

P.S.

アプリケーションルートディレクトリに .env.testing というファイルを置くと、ユニットテストを動かした時だけそのファイルに書かれた環境定数を読み込んでくれますよ。
環境定数に関しては phpunit.xml でも記述出来るので、場合に応じて使い分けましょう(各環境でのテスト用DBコネクションを .env.testing に書いてみたり)。

ブログ記事検索

このブログについて

このブログは、札幌市・仙台市の「株式会社インフィニットループ」が運営する技術ブログです。 お仕事で使えるITネタを社員たちが発信します!