Laravel入門 ToDo一覧画面でのFeatureテスト(第10回)

2021-01-01
8 min read

Laravel でのテストの基本。Feature テストでリスト表示画面をテストします。

テスト

前回までに、ToDo を一覧で表示するリスト画面を作成して、テストデータの準備方法を学びました。

今回のテーマは、自動テストです。前回までは、動作が正しいことを目視で確認していましたが、テストコードを作成することで自動で行えるようにします。

自動化することで、修正や拡張した後に、前は正しく動いたものが修正後に動かなくなったという問題を防ぐことができます。

テストケース

仕様から考えられるテストケースは下記になります。

  • 表示されたタスクの件数が正しいこと
  • ステータスが未着手のタスクのみ表示されていること
  • 未着手のデータがないときはリストが空であること
  • タスクテーブルにレコードがない場合は空で表示されること

これらを自動でテストできるようにしていきます。

テストデータの修正

始めに、4 つのテストケースのためのデータを用意します。

ケース別にテストデータを準備しやすいように前回作成したモデルファクトリに修正を加えます。

database/factories/TaskFactory に以下を追記してください。

$factory->state(Task::class, 'Ready', function (Faker $faker) {
    return ['status' => 1,'title' => 'Ready'];
});

$factory->state(Task::class, 'Doing', function (Faker $faker) {
    return ['status' => 2,'title' => 'Doing'];
});

$factory->state(Task::class, 'Done', function (Faker $faker) {
    return ['status' => 3,'title' => 'Done'];
});

$factory->state(Task::class, 'notReady', function (Faker $faker) {
    return ['status' => 4,'title' => 'notReady'];
});

上記はステータス毎のデータを定義したものです。 このようにすることで、ステータス毎にテストデータを簡単に用意できます。 例えば、未着手を 5 件、そのほかのスタータスを 1 件ずつ用意するコードは下記になります。

 factory(Task::class,5)->state('Ready')->create();
 factory(Task::class,1)->state('Doing')->create();
 factory(Task::class,1)->state('Done')->create();
 factory(Task::class,1)->state('notReady')->create();

具体的な使用方法はテスト実装とともに解説します。

テスト実装

テストクラスを作成していきます。

Laravel の公式ドキュメントの翻訳は下記です。
https://readouble.com/laravel/7.x/ja/testing.html

artisanコマンドで、テストコントローラーの雛形を作成します。

php artisan make:test TaskControllerTest

上記コマンドの実行で、tests/Feature/ 以下に、TaskControllerTest.phpが生成されます。

<?php

namespace Tests\Feature;

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

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

Laravel で標準で作られるテストケースはフューチャーテストになります。 PHP は、基本的にリクエストを受け取りレスポンスを返すための言語です。 そして、この一連の流れを正しく動作するかを確認するのは、フューチャーテストになります。 Laravel では、フューチャーテストを簡単に行うための機能がいくつも用意されています。 もちろんクラス単位のテストとしてユニットテストも行うことができます。

TaskControllerTest.phpにテストケースを実装していきます。

始めに、「表示されたタスクの件数が正しいこと」というケースを実装します。 testReadyListOnlyというメソッドを追加します。

public function testReadyListOnly()
{

   factory(Task::class,5)->state('Ready')->create();
   factory(Task::class,1)->state('Doing')->create();
   factory(Task::class,1)->state('Done')->create();
   factory(Task::class,1)->state('notReady')->create();


    $response = $this->get("/");
    $response->assertStatus(200);
    $response->assertViewHas('tasks');
    $tasks = $response->original['tasks'];

    $this->assertEquals(5, count($tasks));

}

また、use App\Task;use Tests\TestCase;の下に追加してください。

メソッド名がtestから始まるものがテストケースとして実行されます。

factoryメソッドでテストデータをdatabaseに登録します。 ここでは、未着手 5 件、着手 1 件、完了 1 件、中止 1 件となります。

$this->get("/")で、home にアクセスし、レスポンス(サーバからの応答)を得ます。 その後、以下 3 つを確認しています。

  • assertStatus(200)で、レスポンスのステータスコードが 200 であること
  • assertViewHas('tasks')で、レスポンスの VIEW に配列のtasksが含まれること
  • $response->original['tasks']で、レスポンスからtasksの配列を取得して、assertEquals(5, count($tasks))にて、5 件であること

すべてTRUEのときにテストが OK となります。

テスト環境の設定

テスト環境を設定します。

phpunit.xmlの設定

todo 直下のphpunit.xmlのデータベース接続情報を変更します。

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true">
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>

        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
        </whitelist>
    </filter>
    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <!-- <server name="DB_CONNECTION" value="pgsql"/> -->
        <!-- <server name="DB_DATABASE" value="todo"/> -->
        <server name="MAIL_DRIVER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
    </php>
</phpunit>

DB_CONNECTION と DB_DATABASE をコメントアウトします。

.env.testingの作成

.env をコピーして、.env.testingを作成します。

$ cp .env .env.testing

APP_KEY を設定します。

$ php artisan key:generate --env=testing

変更後に、Laravel のキャッシュをクリアします。

php artisan config:clear

テスト実行

テストを実行します。

$ ./vendor/bin/phpunit 

Laravel7の場合は、下記でも実行できます。

$ php artisan test

フューチャーテストのみ実行するなら下記です。 上記のコマンドに、オプションを付与します。

$ ./vendor/bin/phpunit tests/Feature/

Laravel7の場合は、下記でも実行できます。

$ php artisan test tests/Feature/

特定のテストのみ実行するなら下記となります。

$ ./vendor/bin/phpunit  tests/Feature/TaskControllerTest.php

Laravel7の場合は、下記でも実行できます。

$ php artisan test tests/Feature/TaskControllerTest.php

特定のテストのみ実行した結果は、下記のようにTests: 2, Assertions: 4, Failures: 1.になり、testReadyListOnlyがエラーとなっています。

.F                                                                  2 / 2 (100%)

Time: 00:00.115, Memory: 32.01 MB

There was 1 failure:

1) Tests\Feature\TaskControllerTest::testReadyListOnly
Failed asserting that 80 matches expected 5.

/var/www/html/todo/tests/Feature/TaskControllerTest.php:39

FAILURES!
Tests: 2, Assertions: 4, Failures: 1.

これは、tasksテーブルに 5 件を超えた数のタスクがあったためです。 今回、テストデータとして5件追加しましたが、そもそも既にタスクテーブルにデータが存在していたためです。

事前にtasksテーブルを空にしておく必要がありました。

都度テストを行うたびに、手動でtasksテーブルをtruncateするのは面倒かつ忘れがちになります。 こういう場合、Laravel のテストでは、RefreshDatabaseを使用することで、テストが終わるとともに、データベースを初期化できます。

テストのための初期化

RefreshDatabaseを使用すると各テストメソッド終了毎に/database/migrations以下のMigrationが実行されます。 結果、次回のテストが実行されるときは、すべてのテーブルが空の状態となります。

TaskControllerTest.phpを修正します。


<?php
namespace Tests\Feature;

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

class TaskControllerTest extends TestCase
{

    use RefreshDatabase;

注意点としては、すべてのテーブルが空になるということは、マスタデータも消えてしまうことになります。 マスタデータが必要な場合は、テストクラスにsetupメソッドを追加し、そのメソッド内にてマスタデータのシーダーを呼ぶます。 そうすることで、テスト実行前にマスタデータがテーブルに登録されます。


<?php
    protected function setUp(): void
    {
        parent::setUp();
        $this->artisan('db:seed');
    }

今回、作成するものには、マスタデータを使用していないためsetupメソッドは不要です。

テスト用のデータベース作成

自動テスト終了後はデータベースが初期化状態となると、コーディング中の動作確認をするためのデータがなくなってしまいます。 そこで、今までのデータベースに加えて、自動テスト用のデータベースを作ります。

$ sudo -i -u postgres
$ createdb -O developer todo_test
$ exit

.env.testingDB_DATABASEを修正します。

DB_DATABASE=todo_test

テスト用のデータベース内にテーブルを作成します。

$ php artisan migrate --env=testing

マスタデータなど、テスト実施に必要な共通のデータをがある場合は、 setUp()メソッド内の$this->artisan('db:seed');を修正する必要があります。 テスト用のデータベースに対してデータを登録するために下記のように修正します。

\Artisan::call('db:seed', ['--env' => 'testing',]);

今回、作成するものには、マスタデータを使用していないため上記は不要です。

テストの再実行

$ ./vendor/bin/phpunit  tests/Feature/TaskControllerTest.php
developer@Ryzen:/var/www/html/todo$ ./vendor/bin/phpunit tests/Feature/TaskControllerTest.php
PHPUnit 9.5.1 by Sebastian Bergmann and contributors.

Warning:       Your XML configuration validates against a deprecated schema.
Suggestion:    Migrate your XML configuration using "--migrate-configuration"!

.F                                                                  2 / 2 (100%)

Time: 00:00.236, Memory: 36.01 MB

There was 1 failure:

1) Tests\Feature\TaskControllerTest::testReadyListOnly
Failed asserting that 8 matches expected 5.

/var/www/html/todo/tests/Feature/TaskControllerTest.php:44

FAILURES!
Tests: 2, Assertions: 4, Failures: 1.

テストが失敗します。 5件表示されるはずが8件表示されています。 未着手だけを表示させるはずが全件表示されてしまっています。

エラーの修正

TaskController.php の index() メソッドを修正します。

public function index()
{
  $tasks = Task::Where('status',1)->get();
   return view('index', compact('tasks'));
}

テストの再実行

developer@Ryzen:/var/www/html/todo$ ./vendor/bin/phpunit tests/Feature/TaskControllerTest.php
PHPUnit 9.5.1 by Sebastian Bergmann and contributors.

Warning:       Your XML configuration validates against a deprecated schema.
Suggestion:    Migrate your XML configuration using "--migrate-configuration"!

..                                                                  2 / 2 (100%)

Time: 00:00.223, Memory: 36.01 MB

OK (2 tests, 4 assertions)

手間をかける必要がありますが、この一手間で楽になります。

ところで、テスト実行時にWarningが出ていました。設定ファイルに廃止された項目があるということです。これを修正します。 メッセージに従い、./vendor/bin/phpunit --migrate-configuration を実行します。

developer@Ryzen:/var/www/html/todo$ ./vendor/bin/phpunit --migrate-configuration
PHPUnit 9.5.1 by Sebastian Bergmann and contributors.

Created backup:         /var/www/html/todo/phpunit.xml.bak
Migrated configuration: /var/www/html/todo/phpunit.xml

再実行して、Warningが表示されないことを確認します。

developer@Ryzen:/var/www/html/todo$ ./vendor/bin/phpunit tests/Feature/TaskControllerTest.php
PHPUnit 9.5.1 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 00:00.224, Memory: 36.01 MB

OK (2 tests, 4 assertions)

Warningも消えてスッキリしました。

残りのテストケースの実装

  • (実装済み)表示されたタスクの件数が正しいこと
  • ステータスが未着手のタスクのみ表示されていること
  • 未着手のデータがないとき
  • タスクテーブルにレコードがない場合は空で表示されること

ステータスが未着手のタスクのみ表示されていること

public function testReadyListAll()
{

   factory(Task::class,5)->state('Ready')->create();
   factory(Task::class,1)->state('Doing')->create();
   factory(Task::class,1)->state('Done')->create();
   factory(Task::class,1)->state('notReady')->create();

    $response = $this->get("/");
    $response->assertStatus(200);

    $response->assertViewHas('tasks', function ($tasks) {
        foreach ($tasks as $task) {
            if ($task->status !== 1) {
                return false;
            }
        }
        return true;
    });

}

未着手のデータがないとき

    public function testReadyListNone()
    {

       factory(Task::class,1)->state('Doing')->create();
       factory(Task::class,1)->state('Done')->create();
       factory(Task::class,1)->state('notReady')->create();

        $response = $this->get("/");
        $response->assertStatus(200);

        $response->assertViewHas('tasks', null);

    }

タスクテーブルにレコードがない場合は空で表示されること

public function testListNone()
{


    $response = $this->get("/");
    $response->assertStatus(200);

    $response->assertViewHas('tasks', null);

}

テストの再実行

developer@Ryzen:/var/www/html/todo$ ./vendor/bin/phpunit tests/Feature/TaskControllerTest.php
PHPUnit 9.5.1 by Sebastian Bergmann and contributors.

.....                                                               5 / 5 (100%)

Time: 00:00.279, Memory: 36.01 MB

OK (5 tests, 10 assertions)
developer@Ryzen:/var/www/html/todo$ 

すべてのテストが OK なら、リスト表示は完成です。 テスト実行時に、–testdox のオプションを付与すると、メソッド名が表示されます。

developer@Ryzen:/var/www/html/todo$ ./vendor/bin/phpunit tests/Feature/TaskControllerTest.php --testdox
PHPUnit 9.5.1 by Sebastian Bergmann and contributors.

Task Controller (Tests\Feature\TaskController)
 ✔ Example
 ✔ Ready list only
 ✔ Ready list all
 ✔ Ready list none
 ✔ List none

Time: 00:00.275, Memory: 36.01 MB

OK (5 tests, 10 assertions)

こっちの方が、達成感が出て気持ちいいですね。

次回への申し送り事項

再度、今回の新規登録画面の仕様を見返して抜け漏れがないかを確認します。

確認の結果、以下が漏れているもしくは先送りされています。

  • 新規登録ボタンの設置
  • 新規登録ボタンの自動テスト

新規登録画面の実装はこれからですが、ボタンは仮の飛び先を作って実装します。 テストは、フューチャーテストではなくブラウザテストを行う必要があります。

次回にて取り組みます。