You are here
Home > Posts tagged "ユニットテスト"

ユーザー認証のテスト(3) Laravel 5.4 ログアウト

前回のログインのテストに対して、今回はログアウトのテスト。

まず、以下を見てください。

   /** @test */
    public function logout()
    {
        // ユーザーを1つ作成
        $user = factory(User::class)->create();

        // 認証済み、つまりログイン済みしたことにする
        $this->actingAs($user);

        // 認証されていることを確認
        $this->assertTrue(Auth::check());

        // ログアウトを実行
        $response = $this->post('logout');

        // 認証されていない
        $this->assertFalse(Auth::check());

        // Welcomeページにリダイレクトすることを確認
        $response->assertRedirect('/');
    }

前回と違うのは、ログインの実行$response = $this->post(..)が今回のテストにはないことです。その代わりに、actingAs()を利用して、プログラムで認証済みにしてしまいます。

actingAs()のおかげで、コードが短くなりわかりやすくなっただけでなく、その使用では以下のようにセッション値を入れたり、リダイレクトすることも可能です。認証された後の、つまりパスワード保護された中の機能のテストではいつも必要なことなので、将来重宝しそうです。

..
 $response = $this->actingAs($user)
                   ->withSession(['foo' => 'bar'])
                   ->get('/');
...

さて、テストとは関係ないですが、ログアウト後はどうして、/loginでなく/にリダイレクトされるのでしょう?また、その変更は可能なのでしょうか?

リダイレクト先は、残念ながら、以下のlaravelのパッケージの中のファイルでハードコードされています。

vendor/laravel/framework/src/Illuminate/Foundation/Auth/AuthenticateUsers.php

...
   /**
     * Log the user out of the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function logout(Request $request)
    {
        $this->guard()->logout();

        $request->session()->invalidate();

        return redirect('/');
    }
...

上のファイルは編集するべきではありません。しかし、ログアウト後のリダイレクト先を変更したいなら、

app/Http/Controllers/Auth/LoginController.php

に、上のlogoutのコードを追加して、リダイレクト先を編集すればよいです。親のメソッドが上書きされて変更されることになります。use Illuminate\Http\Request;の行の追加も忘れないように。以下に全コードを掲載します。

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;

class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

    use AuthenticatesUsers;

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = '/home';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }

    /**
     * Log the user out of the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function logout(Request $request)
    {
        $this->guard()->logout();

        $request->session()->invalidate();

        return redirect('/');
    }
}

最後に今回のテストのコードは以下から利用可能です。

https://github.com/lotsofbytes/larajapan/blob/l54-test/tests/Feature/LoginTest.php

ユーザー認証のテスト(2) Laravel 5.4 ログイン

前回は、

  • ログイン画面にアクセスできる
  • ログインしないで、ホームページにアクセスするとログイン画面にリダイレクトされる

のテストを作成しました。

今回は、同じくログイン画面において以下のテストを作成します。

  • 登録されているユーザーのEメールとパスワードでログインできる
  • 間違ったパスワードでログインした場合、ログイン失敗してエラーが出力される

モデルファクトリ

さて、前回と違って今回は、ログインのテストゆえにテストのデータベースにユーザーのレコードが必要となります。もちろん、手動でレコードを作成しておいてテストも可能です。しかし、テストが複雑になって様々なケースのテストが増えてくると非常に面倒です。

そこで登場するのは、モデルファクトリ。テストのために、必要なデータを必要な数だけ自動生成してDBレコードを作成してくれます。ファクトリの設定は、LaravelのEloquentのモデルを指定するのでとても簡単。以下ではUserのモデルをもとにしています。Fakerのパッケージを利用しているので各項目での詳細はそちらを参照してください。

use Faker\Generator as Faker;

/** @var \Illuminate\Database\Eloquent\Factory $factory */
$factory->define(App\User::class, function (Faker $faker) {
    static $password;

    return [
        'email'          => $faker->unique()->safeEmail, // 複数レコード作成時には重複しないEメールを生成
        'password'       => $password ?: $password = bcrypt('test1234'), // 複数レコード作成時には同じパスワードを生成
        'name'           => $faker->name,
        'remember_token' => str_random(100), //ランダム値
    ];
});

上の設定では、複数のレコード作成時には、emailは重複しないように、パスワードはstatic変数を利用してどのレコードも同じパスワードを作成するようなっています。bcryptは、LaravelのヘルパーでHash::makeと同じです。

日本語のデータを生成したいなら、config/app.phpで、

..
    /*
    |--------------------------------------------------------------------------
    | Application Locale Configuration
    |--------------------------------------------------------------------------
    |
    | The application locale determines the default locale that will be used
    | by the translation service provider. You are free to set this value
    | to any of the locales which will be supported by the application.
    |
    */

    'locale' => 'ja',
    'faker_locale' => 'ja_JP',
..

と、faker_localeを設定してください。

テストでレコードの自動作成

ユニットテストでは、毎回のテストにおいてテストDBを空にしてモデルファクトリで新しいデータを埋めます。これを実行するために、Laravelで2つの異なるメカニズムが用意されています。

  • DataMigrations:Laravelのmigrationのメカニズムを使用して、毎回テストごとにDBテーブルを1から作成する
  • DatabaseTransactions:DBのトランザクションの機能を利用して、毎回テストごとにDBテーブルを空にする

違いは重要で、前者はテストDBにテーブルが何も存在しなくてもOKですが、後者では空のDBテーブルが必要です。

前者は、新規のプロジェクトで最初からLaravelのmigrationを使用してDBを管理しているならお薦めですが、既存のプログラムをLaravelに移行したプロジェクト(私の場合はこれが多い)では、違うメカニズムでDBを管理しているため前者が使用できないので後者となります。

後者では、本DBと同じDB構造とするため、以下のようにmysqldumpなどを使用してコピーする必要あります。

$ mysqludmp -u root larajapan -p -d > larajapan_test.sql
$ mysql -u root larajapan_test -p < larajapan_test.sql

ここでは、すでにmigrationの設定がなされているうえに、DatabaseMigrationsのトレイトをユニットテストの中で宣言します。

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
//use Illuminate\Foundation\Testing\DatabaseTransactions;

use App\User;

class LoginTest extends TestCase
{
    use DatabaseMigrations;
..

ログインテスト

準備整ったところで、テストしてみましょう。まずは、ログイン成功のテスト。

先ほど設定したファクトリを使用して、最初に1つだけユーザーのレコードを作成します。その後、作成したのと同じパスワードを使用してログインを実行。実行後は、認証されていることと、ホームページにリダイレクトされることを確認します。

    /** @test */
    public function valid_user_can_login()
    {
        // ユーザーを1つ作成
        $user = factory(User::class)->create([
            'password'  => bcrypt('test1111')
        ]);

        // まだ、認証されていない
        $this->assertFalse(Auth::check());

        // ログインを実行
        $response = $this->post('login', [
            'email'    => $user->email,
            'password' => 'test1111'
        ]);

        // 認証されている
        $this->assertTrue(Auth::check());

        // ログイン後にホームページにリダイレクトされるのを確認
        $response->assertRedirect('home');
    }

次はログイン失敗のテスト。ここでもファクトリで1つだけレコードを作成しますが、わざと作成したのとは違うパスワードを使用してログインして、エラーメッセージが表示されることを確認します。

    /** @test */
    public function invalid_user_cannot_login()
    {
    // ユーザーを1つ作成
        $user = factory(User::class)->create([
            'password'  => bcrypt('test1111')
        ]);

        // まだ、認証されていないことを確認
        $this->assertFalse(Auth::check());

        // 異なるパスワードでログインを実行
        $response = $this->post('login', [
            'email'    => $user->email,
            'password' => 'test2222'
        ]);

        // 認証失敗で、認証されていないことを確認
        $this->assertFalse(Auth::check());

        // セッションにエラーを含むことを確認
        $response->assertSessionHasErrors(['email']);

        // エラメッセージを確認
        $this->assertEquals('メールアドレスあるいはパスワードが一致しません',
            session('errors')->first('email'));

$responseに使用されるassertの関数は、phpunitからのassertTrueと違い、Laravelで宣言されているものです。

参照としては、本サイトの以下

https://laravel.com/docs/5.4/http-tests#available-assertions

ですが、そこには掲載されていない関数もあるので、以下もチェックしてください。

https://laravel.com/api/5.4/Illuminate/Foundation/Testing/TestResponse.html

最後に、テストを実行してみましょう。

$ vendor/bin/phpunit --filter LoginTest
PHPUnit 5.7.22 by Sebastian Bergmann and contributors.

....                                                                4 / 4 (100%)

Time: 1.09 seconds, Memory: 16.00MB

OK (4 tests, 12 assertions)

エラーもなく成功ですね!

今回のコードは以下から利用可能です。

https://github.com/lotsofbytes/larajapan/blob/l54-test/tests/Feature/LoginTest.php

ユーザー認証のテスト(1) Laravel 5.4

Laravelでのユーザー認証は私のブログの中では最も人気のあるトピックです。今回は、私のLaravelの日本語のレポジトリ(Laravel 5.4)のコードをもとに、ユーザー認証のテストに取り組んでいきます。

テストの種類

テストと言っても、いくつか種類があり、Laravel5.4からはtestsのディレクトリ構造も変わり、

tests
├── CreatesApplication.php
├── Feature
│   └── ExampleTest.php
├── TestCase.php
└── Unit
    └── ExampleTest.php

のようにFeatureUnitの2つのサブディレクトリができました。

Unitには、一般的にはユニットテストと呼ばれるもので、大方は画面の表示を伴わないModelのメソッドに対するテストを作成します。一方、Featureには、機能テストやアクセプタンステストとも言われ、複数のクラスの複数のメソッドが関わる主にコントローラの機能を検証するためで、あたかもユーザーがテストするようなテストを作成します。

私の今までの開発では、比較的作成しやすいUnitテストが主で、Featureテストは皆無に近く、人間のテスターがその仕事を行っています。人間をFeatureテストに置き換える予定はないですが、Laravelが提供するテストの環境がFeatureテストを作成しやすくなってきているので、この機会に習得しようということです。

Laravel5.4では、Featureテストのために、2つのテストのフレームワークが提供されています。

ここでは、まず、追加のパッケージのインストールも要らない、高速なHTTPテストを作成していきます。

準備

github.comにおいて以下のブランチを用意したので利用してください。

https://github.com/lotsofbytes/larajapan/tree/l54-test

インストールは以下を参照してください。

Laravelの日本語のレポジトリ(Laravel 5.4)

設定したら、必ず以下を実行してブランチを変えてください。

$ git checkout l54-test

また、そこでは、phpunit.xmlを編集も必要です。以下を参考にしてください。

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="bootstrap/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>

        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
        </whitelist>
    </filter>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="APP_URL" value="http://localhost"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="DB_HOST" value="localhost"/>
        <env name="DB_DATABASE" value="larajapan_test"/>
        <env name="DB_USERNAME" value="test"/>
        <env name="DB_PASSWORD" value="password"/>
        <env name="MAIL_DRIVER" value="log" />
        <env name="QUEUE_DRIVER" value="sync"/>
    </php>
</phpunit>

テストのDBは.envで設定しているものと違うDBが必要なことに注意してください。

最初のテスト

もとからあるtests/Feature/ExampleTest.phpのファイルを、LoginTest.phpと改名して以下のように編集します。

namespace Tests\Feature;

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

class LoginTest extends TestCase
{
    /** @test */
    public function user_can_view_login()
    {
        $response = $this->get('login');

        $response->assertStatus(200);
    }

    /** @test */
    public function unauthenticated_user_cannot_view_home()
    {
        $this->get('home')
        	->assertRedirect('login');
    }
}

最初のテストuser_can_view_loginは、ログイン画面が閲覧できるかどうかのテストです。
返ってくるHTTPのステータスのコードが200なら、成功ということです。これが404(ページが見つかりません)とかだと何かがおかしいということになります。

次のテストunauthenticated_user_cannot_view_homeは、認証が必要なホームページに、認証もなしにアクセスしてみます。もちろん、アクセスできないでログイン画面にリダイレクトされるはずです。

ここ、メソッドが連結されていることに気づきましたか?

最初のテストの例のように、以下のよう2つの文に分けても書くこともできます。

$response = $this->get('home');
$response->assertRedirect('login');

これらのテストの実行ですが、Laravelのインストールにより、すでにvendorのディレクトリにphpunitのパッケージもインストールされています。

ということで、

$ vendor/bin/phpunit
PHPUnit 5.7.23 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)
Time: 171 ms, Memory: 10.00MB

OK (3 tests, 4 assertions)

と実行してテストは皆成功となります。3 testsとあるのは、app/tests/Unit/ExampleTestがあるからです。
以下のようにフィルタを書ければ、LoginTestの中のテストだけや、user_can_view_loginの1つテストだけの実行も可能です。

$ vendor/bin/phpunit --filter=LoginTest
HPUnit 5.7.23 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 157 ms, Memory: 10.00MB

OK (2 tests, 3 assertions)

$ vendor/bin/phpunit --filter=user_can_view_login
PHPUnit 5.7.23 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 111 ms, Memory: 10.00MB

OK (1 test, 1 assertion)

ユーザー認証(7)スロットルのユニットテスト

前回話したログインのスロットル機能。もちろん手動、つまりブラウザ、で確認はできます。しかし、手動ではいちいち面倒ですね。自動で動作を確認できたらもっと良いです。そこで登場するのがユニットテスト。難しくはないです。チェックしたいのは、

  1. パスワードが正しいときのログインが成功するかのテスト
  2. パスワードを間違えて、失敗回数がデフォルトの5回となったときに、スロットされるかのテスト
  3. その後デフォルトの60秒待って、パスワードが正しいときに成功するかのテスト

他にもいくつか考えられますが、最低限は以上としてテストを作成してみましょう。

注意:パスワードには理解しやすいように日本語としていますが、実際は英数字が一般的です。

class LoginTest extends TestCase
{
    public function testLoginSuccess()
    {
        $this->visit('/auth/login')
            ->type('test@gmail.com', 'email')
            ->type('正しいパスワード', 'password')
            ->press('保存')
            ->see('ホームページ');
    }

    public function testLoginThrottle()
    {
        for($i=1;$i <=5; $i++)
        {
            $this->visit('/auth/login')
                ->type('test@gmail.com', 'email')
                ->type('間違ったパスワード', 'password')
                ->press('ログイン')
                ->see('Eメールとパスワードにマッチするレコードがありません。');
        }

        $this->visit('/auth/login')
            ->type('test@gmail.com', 'email')
            ->type('間違ったパスワード', 'password')
            ->press('ログイン')
            ->see('ログインの失敗回数が設定を超えました。次回のログインまで60秒お待ちください。');

        sleep(10);

        $this->visit('/auth/login')
            ->type('test@gmail.com', 'email')
            ->type('正しいパスワード', 'password')
            ->press('ログイン')
            ->see('ログインの失敗回数が設定を超えました。');

        sleep(50);

        $this->visit('/auth/login')
            ->type('test@gmail.com', 'email')
            ->type('正しいパスワード', 'password')
            ->press('ログイン')
            ->see('ホームページ');
    }
}

最初のテスト、testLoginSuccessは、正しいパスワードを入力したときのテストで、その後「ホームページ」という文が入ったページへ行くとする仮定。

次のtestLoginThrottleは、スロットルされるまでの失敗回数の確認とスロットル解除の確認のテスト。まず間違ったパスワードを入れて5回失敗させます。画面には、「Eメールとパスワードにマッチするレコードがありません」のエラーメッセージが表示されます。その後、さらに失敗すると、今度はログインをスロットルしたメッセージが表示されます。ここから60秒経過しないと次のログインができません。確認のために10秒後に正しいパスワードでログインしてみましょう。そこでは、さらに「ログインの失敗回数が設定を超えました。」を含むエラーメッセージがでます。残りの秒数は誤差が出ると思うので、その部分の確認はしません。そして、50秒後(つまり、スロットル開始から60秒後)には、正しいパスワードでログインが成功となるはずです。

さて、ここで注意です。Laravelに含まれるphpunitのテストの設定ファイルは、以下の内容です。

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="bootstrap/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Application Test Suite">
            <directory>./tests/</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist>
            <directory suffix=".php">app/</directory>
        </whitelist>
    </filter>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="QUEUE_DRIVER" value="sync"/>
    </php>
</phpunit>

前回話したように、ログイン失敗回数や現在スロットルされていることなどの情報は、デフォルトではファイルを使用して行っています。しかし、テストでは CACHE_DRIVERは配列設定となっています。私のテストでは、そのためか上のテストはスロットル期間終了後に成功とはなりませんでした。どうも残りの秒数のカウントが負となっていしまい、うまくいかない。

この設定をphpunit.xmlでarrayからfileあるいはdatabaseに変更とすると、うまく行きました。

入力の空白文字をトリム(2)全角スペース

前回の「空白文字の入力をトリムする」の続きです。

まず、動作確認を簡単にするために、ユニットテストを作成しましょう。

入力画面のユニットテストをもとに、いろいろなデータをテストできるように、データプロバイダーを使用します。


use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class SignupTest extends TestCase
{
    use DatabaseTransactions;

    /**
     * @dataProvider providerTrimpInput
     */
    public function testTrimpInput($last_name, $expected)
    {
        $this->visit('/signup')
            ->type('success@gmail.com', 'email')
            ->type('testtest', 'password')
            ->type('testtest', 'password_confirmation')
            ->type($last_name, 'last_name')
            ->type('太郎', 'first_name')
            ->press('保存')
            ->see('会員登録完了');

        $member = \App\Member::where('email', 'success@gmail.com')->first();

        $this->assertEquals($expected, $member->last_name);
    }

    public function providerTrimpInput()
    {
        return [
            [ '山田', '山田'],
            [ ' 山田 ', '山田'],   //半角スペース
            [ ' 山田 ', '山田'], //全角スペース
        ];
    }
}

ユニットテストのデータプロバイダーは、テストとデータを分けることにより、1つのテストでいろいろなデータのテストが可能となります。必要なのは、コメントのphpDocの@dataProviderでデータを供給する関数名を指定することと、そのデータ供給の関数を作成することです。この例では3つのデータを用意することにより、3回テストが実行されます。

テスト実行結果は、以下です。

PHPUnit 4.8.9 by Sebastian Bergmann and contributors.

..F

Time: 360 ms, Memory: 26.50Mb

There was 1 failure:

1) SignupTest::testTrimpInput with data set #2 (' 山田 ', '山田')
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'山田'
+' 山田 '

/vol1/usr/www/shop/webdocs/demo/tests/SignupTest.php:27

FAILURES!
Tests: 3, Assertions: 12, Failures: 1.

2番目の半角のスペースのデータはトリムされ成功となり、3番目の全角スペースが前後にあるデータでは失敗となります。

さて、全角スペースはどう対応しましょうか?

再度、trim関数の仕様を見てみましょう。

 string trim ( string $str [, string $character_mask = " \t\n\r\0\x0B" ] )

この関数は str の最初および最後から空白文字を取り除き、 取り除かれた文字列を返します。
2番目のパラメータを指定しない場合、 trim()は以下の文字を削除します。

    " " (ASCII 32 (0x20)), 通常の空白。
    "\t" (ASCII 9 (0x09)), タブ。
    "\n" (ASCII 10 (0x0A)), リターン。
    "\r" (ASCII 13 (0x0D)), 改行。
    "\0" (ASCII 0 (0x00)), NULバイト
    "\x0B" (ASCII 11 (0x0B)), 垂直タブ

そう、2番目のパラメータに全角スペースを入れてあげればよいですね。

trim($val, ' \t\n\r\0\x0B ');//全角スペースは最後の空白文字

しかし、これはテストしたところうまくいきませんでした。全角のスペースが入る値には対応するようですが、半角の文字列では文字列によっては空白文字でないものもトリムしてしまいます。trimの関数はユニコード対応でないようですね。それならば、mb_trimと思いますが、残念ながらこれが存在しません。

いろいろ調べたところ、preg_replaceが使えそうです。

パターンは、

'/(^\s+)|(\s+$)/u'

\sは空白文字のエスケープシーケンスであり、ドキュメントでは以下の記述があります。

エスケープシーケンス

空白文字とは HT (9)、LF (10)、FF (12)、CR (13)、スペース (32) のことです。 しかし、ロケールを指定したマッチングを行った場合には、128から255までのコードポイントの文字 (たとえば NBSP (A0)) も空白文字とみなされる可能性があります。

ということで、trimで削除する空白文字とともにユニコードの全角スペースも削除してくれます。

ミドルウェアを書き直すと、

namespace App\Http\Middleware; 

use Closure; class TrimInput {

    /** 
     * Handle an incoming request. 
     * 
     * @param \Illuminate\Http\Request $request 
     * @param \Closure $next 
     * @return mixed 
     */ 
    public function handle($request, Closure $next) { 

        $input = $request->all();

        $trimmed = [];

        foreach($input as $key => $val)
        {
            $trimmed[$key] = preg_replace('/(^\s+)|(\s+$)/u', '', $value);
        }

        $request->merge($trimmed);

        return $next($request);
    }
}

先のユニットテストの結果は?

PHPUnit 4.8.9 by Sebastian Bergmann and contributors.

...

Time: 359 ms, Memory: 26.50Mb

OK (3 tests, 12 assertions)

成功です!

次回は、配列の入力値の個々の値の空白文字のトリムの対応です。

入力画面のユニットテスト

ユニットテスト(PHPの場合は、phpunit)を使い始めて、2,3年。その重要さは理解しているものの、つい最近まで、コードの大変さによりなかなか多用はしていませんでした。

データベース絡みや入力画面絡みのテスト、書くのはやっかいです。しかし、ウェブのアプリの開発には、データベースや入力画面があって当然。ここのテストを自動化をせずにいったいどこでする?とも思う。

しかし、どうしてやっかいなのでしょう、これら?

例えば、前回の会員登録の件では、テストとして考えられるのは、

まずは、エラーを出して確認するテストたち:

  • 必須の項目に値がないときにエラーとなるか。
  • EメールにEメールでないものを入力したときにエラーとなるか。
  • すでに登録した会員のEメールを入れたときにエラーとなるか(重複チェック)。

そして、エラーがなく、会員登録成功したときの確認のテストも必要です。

前者のエラーを出して確認するテストは、コントローラーでなくモデルに入れてテストが可能かもしれません。ララベルのバリデーションはすでに一貫しているし、カスタマイズのバリデーションも同様にテスト可能です。

しかし、それらユニットテストは個々のバリデーションをテストするだけで、それら集合した入力画面に対してのファンクショナルテストやアクセプタンステストは難しいです。あたかもユーザーが入力するようなテストです。

さらに、後者のエラーが出ない成功の確認のテストは、実際にDBにレコードが作成されてしまいます。つまり1回成功テストすると、次回は重複でエラーとなり成功のテストができなくなります。もちろん、毎回レコードを削除するという手段もありますが(これはDBテストとして将来に紹介します)、違うDBを用意するとかお膳立てがかなり面倒です。

もちろん、ここで、ララベルがそのフレームワークを活かしたユニットテストの登場です(注意:ララベル5.1での前提です)。

まずは、Eメールアドレスの重複による失敗のテストです。

class SignupTest extends TestCase
{
    public function testSignupFail()
    {
        $this->visit('/signup')
            ->type('dup@gmail.com', 'email')
            ->type('testtest', 'password')
            ->type('testtest', 'password_confirmation')
            ->type('山田', 'last_name')
            ->type('太郎', 'first_name')
            ->press('保存')
            ->dontSee('会員登録完了');
    }
}

このテストは、ユーザーが以下のような画面を経験したと同じ状況をテストします。すでにDBにdup@gmail.comが存在していると仮定です。

fail

16行目の “dontSee(‘会員登録完了’);” では、エラーになるために成功時の「会員登録完了」の文字が画面には見えないよ、つまりdon’t seeということです。

今度は、成功時のテストですが、

use IlluminateFoundationTestingWithoutMiddleware;
use IlluminateFoundationTestingDatabaseMigrations;
use IlluminateFoundationTestingDatabaseTransactions;

class SignupTest extends TestCase
{
    use DatabaseTransactions;

    public function testSignupSuccess()
    {
        $this->visit('/signup')
            ->type('success@gmail.com', 'email')
            ->type('testtest', 'password')
            ->type('testtest', 'password_confirmation')
            ->type('山田', 'last_name')
            ->type('太郎', 'first_name')
            ->press('保存')
            ->see('会員登録完了');
    }
}

先の失敗の例と違って、今度はdon’t seeでなくsee(‘会員登録完了’)となっています。

しかし会員登録完了なのにDBのレコードは作成されません。そう、7行目のuse DatabaseTransactionsの宣言により、レコードの作成を試みるもののDBのトランザクション機能を使用してわざと変更をロールバックさせています。これにより作成あるいは変更されたデータを手動で戻さずに、何回でもテストの実行が可能となるわけです。もちろん、トランザクション機能があるDB、mysqlならinnodbの使用でないとできないことです。

先の2つテストを合わせて、実行すると、「OK」という結果です。

$ phpunit tests/SignupTest
PHPUnit 4.8.2 by Sebastian Bergmann and contributors.

..

Time: 313 ms, Memory: 27.25Mb

OK (2 tests, 7 assertions)
Top