Lesson 14

テスト

Lesson 14 Chapter 1
Laravelのテストの仕組み

Lesson14ではLaravelのテストについて学習を行っていきましょう。

テストとは

テストとは実際に記述したプログラムが正常に動作しているかの確認を行う作業になります。開発者は機能を実装するだけでなく、実装した機能に不備がないか、又、動作上問題はないか等の確認を行いリリース後にバグや予期しない動作を未然に防ぐことで、より良いサービスの構築を心がける必要があります。Lesson11以降で作成したTODOアプリケーションは実際に動かしてブラウザを確認しながら正常にプログラムが動作をしているのかを確認してきましたが、Laravelではテスト用のフレームワーク「PHP Unit」が標準で搭載されており、PHP Unitを利用することで機能の実装テストを実施することも可能です。

ユニットテスト

今回行うテストがユニットテストと呼ばれるテストです。ユニットテストとは関数やメソッドごとに行うテストのことを指し、単体テストとも呼びます。そしてユニットテストは基本的に関数やメソッドを作成した段階で行うのが一般的となっています。Laravelではインストールした際にPHP Unitの実行環境が整っていますので、早速デフォルトのテストファイルを確認してみましょう。

tests > Feature > ExampleTest.php

ExampleTest.php
<?php

namespace Tests\Feature;

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

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function test_the_application_returns_a_successful_response()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

任意の処理をtest_the_application_returns_a_successful_responseメソッドへ記述することにより、テストを実行することが可能です。しかし今回はデフォルトのテストファイルは使用せず、各処理ごとにテストファイルを作成し、テストを実行していきます。

ユニットテストのメリット

ユニットテストのメリットは主に3つ存在します。

メリット1

関数やメソッドを作成しながらテストを実行していく為、早い段階でエラーなどの問題箇所を発見することが可能です。

メリット2

テストはコマンドの実行で行うことができ、又、複数のテストファイルを同時に実行することが可能です。その為、確認の時間を最小限に抑えることができます。

メリット3

テストファイルへコードとして保存しておく為、必要に応じて気軽に何度もテストを実行することが可能です。

chapter2以降では実際にテストファイルを作成していきましょう。

Lesson 14 Chapter 2
テスト用の環境ファイル作成

chapter2では一つテスト用にファイルを作成し、簡単な動作確認を行ってみましょう。テストファイルはコマンド一つで準備することが可能です。

ターミナル、コマンドプロンプト
php artisan make:test SampleTest

作成されたファイルを確認していきます。テストファイルは「○◯Test」と末尾がTestとなることが一般的な為、覚えておきましょう。

tests > Feature > SampleTest.php

SampleTest.php
<?php

namespace Tests\Feature;

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

class SampleTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_example()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

デフォルトではwelcomeページ(http://127.0.0.1:8000/)へアクセスした結果をテストするコードとなっています。今回はSampleTestを使用して、ログイン画面のルーティングが正しく設定できているかどうかの確認を行ってみます。test_exampleメソッドの記述を修正してください。

SampleTest.php
public function test_example()
{
  $response = $this->get('/login');

  $response->assertStatus(200)->assertViewIs('auth.login');
}

$this->get('/login')

getの引数にはテストを実行したいURLを設定します。

assertStatus(200)

レスポンスが正常かどうかのチェックを行っています。assertStatus()の引数にはチェックしたいステータスコードを設定します。処理に問題がないかテストを行うので、基本的には200を設定しておきましょう。

assertViewIs('auth.login')

ルーティングに対してテストを実行するメソッドになります。こちらはLaravelで予め定義されているメソッドになり、引数にはbladeのファイル名を指定します。ログイン画面の場合はauthフォルダの配下にlogin.bladeが存在している為、フォルダ名.ファイル名とする必要がありますので覚えておきましょう。

テストの実行

テストの実行はコマンド一つで行うことができます。以下コマンドを実行しましょう。

ターミナル、コマンドプロンプト
php artisan test
実行結果
ターミナル、コマンドプロンプト
PASS  Tests\Feature\SampleTest
✓ example
Tests:  1 passed

特に問題がなければこのようなテストの結果となります。

エラー時の動作確認も行ってみましょう。以下修正してください。

SampleTest.php
public function test_example()
{
  $response = $this->get('/login');

  // authを消す
  $response->assertStatus(200)->assertViewIs('login');
}

もう一度テストを実行してみます。

ターミナル、コマンドプロンプト
php artisan test
実行結果
ターミナル、コマンドプロンプト
-'login'
+'auth.login'

Tests:  1 failed

修正箇所もテストの結果として返ってきました。簡単な動作確認は以上となります。

ダミーデータの作成

次のchapterでは実践的なテストを行っていきます。データの操作を行う為、既存のデータではなくダミーデータを作成し、そのダミーデータをもとにテストを実行していきますのでその準備を行いましょう。

デフォルトのダミーデータ

Laravelにはデフォルトでダミーデータを作成する為のUserFactoryが用意されており、任意の場所でUserFactoryクラスを呼び出すことでダミーデータの作成を行うことができます。ファイルの中身を確認してみましょう。

databases > factories > UserFactory.php

UserFactory.php
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
 */
class UserFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
            'remember_token' => Str::random(10),
        ];
    }

    /**
     * Indicate that the model's email address should be unverified.
     *
     * @return static
     */
    public function unverified()
    {
        return $this->state(fn (array $attributes) => [
            'email_verified_at' => null,
        ]);
    }
}

Laravelにはfakeメソッドが用意されており、fakeメソッドを使用することでランダムなデータを作成することが可能です。実際の作成方法については次のchapterでご紹介していきます。

TodoFactoryの作成

UserFactoryはデフォルトで存在していますが、今回はTODOのダミーデータも使用する為、ダミーデータを作成してくれるファイルTodoFactoryを作成していきましょう。

ターミナル、コマンドプロンプト
php artisan make:factory TodoFactory

databases > factories > TodoFactory.php

修正前
TodoFactory.php
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Todo>
 */
class TodoFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
            //
        ];
    }
}

修正後
TodoFactory.php
<?php

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Todo>
 */
class TodoFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
            "user_id" => User::Factory(),
            "todo_time" => fake()->time("10:00:00"),
            "todo_priority" => fake()->numberBetween(1, 3),
            "todo" => fake()->text("10"),
            "completed_flag" => fake()->randomElement([0, 1]),
        ];
    }
}

returnの配列の中には中には連想配列の形式「カラム名 => データ」で指定してあげます。user_idにはダミーユーザーのID、todo_timeには時間指定で10:00、todo_priorityには1~3のランダムな数字、todoは10文字以内のランダムな文字列、completed_flagには0か1の整数のルールのもとデータ作成を設定しています。「fake()->」に続くメソッドはLaravelで用意されているメソッドで、作成したいデータを指定してあげます。

以上でテストを実行する環境の準備が整いました。

Lesson 14 Chapter 3
テストの作成

chapter3では作成したTODOアプリケーションのそれぞれの機能をテストするファイルを作成していきましょう。

実行前準備

これからテストを行っていく上で予めCSRFトークンの設定を変更しておく必要があります。以下修正を行いましょう。

app > middleware > VerifyCsrfToken.php

VerifyCsrfToken.php
<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array
     */
    protected $except = [
        //
    ];

    // 追加
    public function handle($request, \Closure $next)
    {
        if (env('APP_ENV') !== 'testing') {
            return parent::handle($request, $next);
        }

        return $next($request);
    }
}
phpunit.xml
<php>
    <env name="APP_ENV" value="testing"/>
    <env name="BCRYPT_ROUNDS" value="4"/>
    <env name="CACHE_DRIVER" value="array"/>
    <env name="MAIL_MAILER" value="array"/>
    <env name="QUEUE_CONNECTION" value="sync"/>
    <env name="SESSION_DRIVER" value="array"/>
    <env name="TELESCOPE_ENABLED" value="false"/>
    <!-- 追加 -->
    <server name="APP_ENV" value="testing"/>
</php>

これらの変更はCSRFトークンを無視する設定になります。テストにおいてトークンは不要な為です。設定を行わない場合はテスト実行後にトークン不一致となり419エラーとなりますので注意しましょう。

ログインのテスト作成

ログインテスト用のファイルを作成し、記述を追記します。

ターミナル、コマンドプロンプト
php artisan make:test LoginTest
修正後
LoginTest.php
<?php

namespace Tests\Feature;

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

class LoginTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_example()
    {
        // ①
        $user = User::factory(User::class)->create([
            "name" => fake()->name(),
            "email" => fake()->unique()->safeEmail(),
            "password" => bcrypt("password"),
        ]);

        // ②
        $response = $this->from("/login")->post("/login/check", [
            "email" => $user->email,
            "password" => "password",
        ]);

        // ③
        $response->assertRedirect("/top");

        // ④
        $this->assertAuthenticatedAs($user);
    }
}

①ダミーのユーザーデータを作成

「モデル::Factory()->create()」とすることでUserFactory.phpを使用しダミーデータの作成を行っています。nameにはランダムな名前を、emailには重複の無いダミーメールアドレスを、passwordにはハッシュ化されたパスワードを作成する設定の元、データの作成を行っています。

②ログイン処理のテスト

「$this->from("/login")」は実際のログイン画面URLを引数に入れており、「post("/login/check")」でログイン処理のルーティングを呼び出しています。こちらでは作成したユーザーを認証するテストを行っています。

③リダイレクト先の確認

今回の仕様ではログイン成功後にTODO一覧ページ(/top)へ遷移するよう指定しています。今回のテストログイン後、/topへアクセスできているか確認を行っています。

④ログインしたユーザーは認証されているか確認

ログイン後、認証ができているか確認を行っています。

webアプリの仕様でもユーザーの新規登録を行い、ログイン機能で認証のチェックをします。認証が取れた場合は/topへ遷移する処理になっているので、実際の処理を辿るようなイメージでテストの実行を行っていきます。以上でログイン用のテストファイルは完成です。

TODO作成のテスト作成

TODO作成用のテストファイルを作成し、記述を追記します。

ターミナル、コマンドプロンプト
php artisan make:test TodoStoreTest
修正後
TodoStoreTest.php
<?php

namespace Tests\Feature;

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

class TodoStoreTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_example()
    {

      // ①
        $user = User::factory(User::class)->create([
            "name" => fake()->name(),
            "email" => fake()->unique()->safeEmail(),
            'password' => bcrypt('password'),
        ]);

        // ②
        $this->actingAs($user)->get('/create');

        // ③
        $todo = Todo::factory(Todo::class)->count(3)->create();

        // ④
        $count = count($todo);

        // ⑤
        $this->assertEquals(3, $count);
    }
}

①ダミーのユーザーデータを作成

TODOの作成処理は認証されているユーザーのみ行うことが可能な為、認証用にユーザーを作成しています。

②作成したユーザーの認証

actingAsメソッドで引数に指定されたユーザーを認証済みの状態にします。又、getでは該当のページを指定しています。ユーザー作成を行うページをルーティングでは「/create」と指定しているので、今回処理を実行する画面を指定しています。

③TODOを3つランダムに作成

作成したTodoFactoryを呼び出し、ランダムにtodoのデータを3つ作成しています。

④作成したTODOをカウント

③で作成したtodoのデータをカウントしています。

⑤作成したTODOの数が3つか確認

assertEqualsでデータの比較を行うことができます。作成したデータが3つ存在しているかここで判定をし、問題なく作成処理が実行されているかの確認を行っています。

TODO一覧取得のテスト作成

TODO一覧取得用のテストファイルを作成し、記述を追記します。

ターミナル、コマンドプロンプト
php artisan make:test TodoGetTest
修正後
TodoGetTest.php
<?php

namespace Tests\Feature;

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

class TodoGetTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_example()
    {
        // ①
        $user = User::factory(User::class)->create([
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'password' => bcrypt('password'),
        ]);

        // ②
        $this->actingAs($user)->get('/top');

        // ③
        $todo = Todo::factory(Todo::class)->create([
            'todo' => 'テストタスク',
        ]);

        // ④
        $this->assertEquals("テストタスク", $todo['todo']);
    }
}

①ダミーのユーザーデータを作成

TODOの一覧表示は認証されているユーザーのみ行うことが可能な為、認証用にユーザーを作成しています。

②作成したユーザーの認証

actingAsメソッドで引数に指定されたユーザーを認証済みの状態にします。

③テストタスクを作成

TodoFactoryを実行し、Todoのダミーデータを作成しています。今回は作成したデータが画面に表示されているかの確認まで行うため、ランダムではなくあえて任意のデータでTODOの作成を行っています。

④作成したTODOと画面に表示されているTODOが一致しているか確認

assertEqualsでデータの比較を行っています。今回の場合、第一引数には判定したい文字列、第二引数には作成したtodoカラムのデータを代入し双方が一致しているかどうか、つまり作成されたTODOのデータが画面に表示されているかどうかの確認を行っています。

TODO更新処理のテスト作成

TODO更新処理用のテストファイルを作成し、記述を追記します。

ターミナル、コマンドプロンプト
php artisan make:test TodoUpdateTest
修正後
TodoUpdateTest.php
<?php

namespace Tests\Feature;

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

class TodoUpdateTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_example()
    {
      // ①
        $user = User::factory(User::class)->create([
            'password' => bcrypt('password'),
        ]);

        // ②
        $this->actingAs($user)->get("/edit");

        // ③
        $todo = Todo::factory(Todo::class)->create();

        // ④
        $todo->update([
            "todo" => "編集後のテスト投稿"
        ]);

        // ⑤
        $this->assertEquals('編集後のテスト投稿', $todo['todo']);
    }
}

①ダミーのユーザーデータを作成

TODOの一覧表示は認証されているユーザーのみ行うことが可能な為、認証用にユーザーを作成しています。

②作成したユーザーの認証

actingAsメソッドで引数に指定されたユーザーを認証済みの状態にします。

③ランダムなTODOを作成

TodoFactoryを実行しランダムなTODOデータを作成しています・

④作成したTODOを更新

③で作成したデータのtodoカラムを「編集後のテスト投稿」にデータを変更する処理を実行しています。

⑤TODO一覧画面で更新した内容が表示されているか確認

assertEqualsでデータの比較を行い、データが更新されているか確認しています。

TODO削除処理のテスト作成

TODO削除処理用のテストファイルを作成し、記述を追記します。

ターミナル、コマンドプロンプト
php artisan make:test TodoDeleteTest
修正後
TodoDeleteTest.php
<?php

namespace Tests\Feature;

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

class TodoDeleteTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_example()
    {
        // ①
        $user = User::factory(User::class)->create([
            'password' => bcrypt('password'),
        ]);

        // ②
        $this->actingAs($user)->get('/todo');

        // ③
        $todo = Todo::factory(Todo::class)->create();

        // ④
        $todo->delete();

        // ⑤
        $this->assertDatabaseMissing("todos", ["id" => $todo->id]);
    }
}

①ダミーのユーザーデータを作成

TODOの一覧表示は認証されているユーザーのみ行うことが可能な為、認証用にユーザーを作成しています。

②作成したユーザーの認証

actingAsメソッドで引数に指定されたユーザーを認証済みの状態にします。

③ランダムなTODOを作成

TodoFactoryを実行しランダムなTODOデータを作成しています・

④作成したTODOを削除

③で作成したデータの作事を実行しています。

⑤TODOが削除されているか確認

assertDatabaseMissingメソッドではデータが存在するかどうかのチェックを行っています。第一引数にはテーブル名を代入し、第二引数にはIDを指定し、指定したIDがtodosテーブル内に存在しないことを確認しています。存在していなければ削除成功の認識になります。

以上でそれぞれの機能に合わせたテストファイル作成が完了しました。テスト実行の方法や確認したい内容によっては記述や構成が異なる場合もありますが、今回はそれぞれの機能が問題なく実行されているかをテストする為の一つの方法だとお考えください。

Lesson 14 Chapter 4
テストの実行

chapter4ではこれまで準備してきたテストファイルを実行していきましょう。

テストの実行

chapter2でも行いましたが、テストの実行方法はコマンド一つで行えます。以下コマンドを実行しましょう。

ターミナル、コマンドプロンプト
php artisan test

テストが実行され、処理やテストファイルの記述、処理内容に誤りがなければ以下のように結果が出力されます。

実行結果
ターミナル、コマンドプロンプト
 PASS  Tests\Feature\LoginTest
  ✓ example

   PASS  Tests\Feature\TodoDeleteTest
  ✓ example

   PASS  Tests\Feature\TodoGetTest
  ✓ example

   PASS  Tests\Feature\TodoStoreTest
  ✓ example

   PASS  Tests\Feature\TodoUpdateTest
  ✓ example

  Tests:  5 passed
  Time:   2.15s

何かしらの誤りがある場合は以下のように出力されます。

ターミナル、コマンドプロンプト

// 該当のファイル名
FAIL  Tests\Feature\TodoUpdateTest
⨯ example

// 行数
at tests/Feature/TodoUpdateTest.php:49

// 指摘箇所
-'編集前のテスト投稿'
+'編集後のテスト投稿'

このように指摘やヒントとなる部分がエラーとなってスローされるので、テストの実行でエラーが検知された場合は実装してきた処理の見直し、作成したテストファイルの確認、エラーの内容をもとに修正や確認を行いましょう。

並列テスト

今回作成したTODOアプリではファイル数も少なく規模も大きくはない為、並列テストのメリットを感じられる部分は薄いですが、最後にテストの並列実行について簡単にご紹介します。

1. テストの並列実行とは

一度に複数のテストを実行することを指します。上記で学習してきたユニットテストでは単体テストになる為、内部的には一つ一つテストを実行していましたが、規模が大きくなり扱うファイル数も増えれば一度に複数ファイルのテストを実行する並列テストのメリットを大きく感じられる点もあります。

2. ユニットテストの実行時間

ユニットテストの実行に要した時間をテスト実行時に確認することができます。コマンドを実行してみましょう。

ターミナル、コマンドプロンプト
php artisan test
実行結果
ターミナル、コマンドプロンプト
Tests:  5 passed
Time:   0.80s

このように5ファイルのテストに0.8秒掛かった結果を確認することが可能です。TODOアプリは簡単なwebアプリな為、テストの実行時間も短く済んでいますが、プロジェクトの規模が大きくなれば単体テストを行うことで10分以上時間を要してしまう可能性もあります。

3. 並列テストの実行方法

並列テストを実行する前にコマンドのオプションに使用するライブラリをcomposerコマンドでインストールする必要があります。

ターミナル、コマンドプロンプト
composer require brianium/paratest --dev

以下が並列テストの実行コマンドになります。

ターミナル、コマンドプロンプト
php artisan test --paralell
出力結果
Time: 00:00.622, Memory: 3.00 MB

規模が大きくなればなるほどメリットを大きく感じられるのが並列テストであり、場合によっては単体テストの1/4ほどにまで実行時間を削減することが可能です。プロジェクトによっては単体テスト、並列テスト、ツールを使用したテスト、画面上でのテストなどテストの実行方法は様々ですが、その現場にあったテストを実行していき、今回の学習では単体テストや並列テストといったテストの方法があることを認識しておきましょう。テストの学習はこれで以上になります。