第3章 タスクの更新と完了

05.既存のデータに依存しないテストへ

ビューテストをPHPUnitコマンドで実行

RefreshDatabaseDatabaseMigrationsを使う前に、少し調整をしなければいけないことがある。

ビューテストはphp artisan duskで実行してきたのだが、 これでは(現在の.envの設定においては)RefreshDatabaseDatabaseMigrationsを使うことができない。

少々面倒な話だが、実行環境にDockerを使っていることに起因している。 php artisan duskで実行すると.envの方が参照される動きになっている。 画面操作のテストについてはブラウザを経由するので、Docker内のWebサーバを経由しているので.envで正しく動くのだが、 ビューテスト内にRefreshDatabaseなどのトレイトを使用すると 開発環境から.envのデータベース設定を参照する動きになってしまう。

しかし、開発環境からは参照できないデータベース設定なので、 データベース操作に失敗してしまいテストがうまく実行できないのだ。

本来ならartisanコマンドのオプションで環境指定ができるはずなのだが、 今回の環境のLaravel Duskでは下記のような状態になってしまう。

$ php artisan dusk --env=testing
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

unrecognized option --env

$ php artisan --env=testing dusk
Cannot open file "dusk.php".

しかし、解決方法はある。 phpunit.xmlを下記のように修正することで、全てのテストを一度に実行できるようになるのだ。 このとき、環境はtestingで実行されるので、データベースへの接続も問題無い。




















 
 
 














<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/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>

        <testsuite name="Browser">
            <directory suffix="Test.php">./tests/Browser</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <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>

これで準備できたので、まずはテストコードの修正と確認をしていこう。

https://readouble.com/laravel/5.5/ja/dusk.html

Laravelのドキュメントに、「環境の処理」として.env.dusk.localを準備する話が出ている。 こちらを準備することで、Laravel Dusk利用時のデータベースの接続先の変更などは可能なようだ。 しかし、今回はこちらでの調整はうまくいかなかった。

また、phpunit.xmlへの追記によってコマンド一つで全てのテストを実行できるようになるため、 利点も大きい。

ビューテストの修正

既存のデータに依存しない形にするために、 ビューテストではDatabaseMigrationsを指定する。

まずは動作を確認してみよう。 tests/Browser/TaskDetailTest.phpを下記のようにする。











 



<?php

namespace Tests\Browser;

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

class TaskDetailTest extends DuskTestCase
{
    use DatabaseMigrations;

()

まずは、これを指定した状態でPHPUnitコマンドで実行をしてみる。

$ vendor/bin/phpunit tests/Browser/TaskDetailTest.php 
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

EE                                                                  2 / 2 (100%)

Time: 5.89 seconds, Memory: 18.00MB

There were 2 errors:

1) Tests\Browser\TaskDetailTest::testShowDetail
Facebook\WebDriver\Exception\NoSuchElementException: no such element: Unable to locate element: {"method":"id","selector":"title"}
  (Session info: headless chrome=66.0.3359.181)
  (Driver info: chromedriver=2.35.528157 (4429ca2590d6988c0745c24c8858745aaaec01ef),platform=Mac OS X 10.13.5 x86_64)

/Users/[ユーザ名]/task-manager/task-manager/vendor/facebook/webdriver/lib/Exception/WebDriverException.php:102
/Users/[ユーザ名]/task-manager/task-manager/vendor/facebook/webdriver/lib/Remote/HttpCommandExecutor.php:320
/Users/[ユーザ名]/task-manager/task-manager/vendor/facebook/webdriver/lib/Remote/RemoteWebDriver.php:535
/Users/[ユーザ名]/task-manager/task-manager/vendor/facebook/webdriver/lib/Remote/RemoteWebDriver.php:175
/Users/[ユーザ名]/task-manager/task-manager/vendor/laravel/dusk/src/ElementResolver.php:281
/Users/[ユーザ名]/task-manager/task-manager/vendor/laravel/dusk/src/ElementResolver.php:83
/Users/[ユーザ名]/task-manager/task-manager/vendor/laravel/dusk/src/Concerns/MakesAssertions.php:507
/Users/[ユーザ名]/task-manager/task-manager/vendor/laravel/dusk/src/Concerns/MakesAssertions.php:480
/Users/[ユーザ名]/task-manager/task-manager/tests/Browser/TaskDetailTest.php:27
/Users/[ユーザ名]/task-manager/task-manager/vendor/laravel/dusk/src/Concerns/ProvidesBrowser.php:67
/Users/[ユーザ名]/task-manager/task-manager/tests/Browser/TaskDetailTest.php:29

2) Tests\Browser\TaskDetailTest::testPost
Facebook\WebDriver\Exception\NoSuchElementException: no such element: Unable to locate element: {"method":"id","selector":"title"}
  (Session info: headless chrome=66.0.3359.181)
  (Driver info: chromedriver=2.35.528157 (4429ca2590d6988c0745c24c8858745aaaec01ef),platform=Mac OS X 10.13.5 x86_64)

/Users/[ユーザ名]/task-manager/task-manager/vendor/facebook/webdriver/lib/Exception/WebDriverException.php:102
/Users/[ユーザ名]/task-manager/task-manager/vendor/facebook/webdriver/lib/Remote/HttpCommandExecutor.php:320
/Users/[ユーザ名]/task-manager/task-manager/vendor/facebook/webdriver/lib/Remote/RemoteWebDriver.php:535
/Users/[ユーザ名]/task-manager/task-manager/vendor/facebook/webdriver/lib/Remote/RemoteWebDriver.php:175
/Users/[ユーザ名]/task-manager/task-manager/vendor/laravel/dusk/src/ElementResolver.php:281
/Users/[ユーザ名]/task-manager/task-manager/vendor/laravel/dusk/src/ElementResolver.php:83
/Users/[ユーザ名]/task-manager/task-manager/vendor/laravel/dusk/src/Concerns/MakesAssertions.php:507
/Users/[ユーザ名]/task-manager/task-manager/vendor/laravel/dusk/src/Concerns/MakesAssertions.php:480
/Users/[ユーザ名]/task-manager/task-manager/tests/Browser/TaskDetailTest.php:46
/Users/[ユーザ名]/task-manager/task-manager/vendor/laravel/dusk/src/Concerns/ProvidesBrowser.php:67
/Users/[ユーザ名]/task-manager/task-manager/tests/Browser/TaskDetailTest.php:53

ERRORS!
Tests: 2, Assertions: 0, Errors: 2.

だいぶ長いエラーメッセージが出てきた。 assertInputValue()で要素が見つからない、と言われている。

DatabaseMigrationsを指定したことにより、 テストの実行の度にデータのリセット(正確にはマイグレーション処理)が行なわれるようになったので、 そもそも$browser->visit()でHTTPステータスが404となっており、 詳細画面そのものが表示されないからだ。

では、テストが開始される度に目的のデータを追加するようにし、そのデータを基にテストを行なうように修正しよう。





 








 

 
 
 
 
 
 
 
 









 













 





 
 





<?php

namespace Tests\Browser;

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

class TaskDetailTest extends DuskTestCase
{
    use DatabaseMigrations;

    private $task;

    protected function setUp()
    {
        parent::setUp();
        $this->task = Task::create([
            'title' => 'テストタスク',
            'executed' => false,
        ]);
    }

    /**
     * Task Detail Test.
     *
     * @throws \Throwable
     */
    public function testShowDetail()
    {
        $this->browse(function (Browser $browser) {
            $browser->visit('/tasks/' . $this->task->id)
                ->assertInputValue('#title', 'テストタスク')
                ->screenshot("task_detail");
        });
    }

    /**
     * Task Post Test.
     *
     * @throws \Throwable
     */
    public function testPost()
    {
        $this->browse(function (Browser $browser) {
            $browser->visit('/tasks/' . $this->task->id)
                ->assertInputValue('#title', 'テストタスク')
                ->type('#title', 'test task')
                ->screenshot('task_post_typed')
                ->press('更新')
                ->pause(1000)
                ->assertPathIs('/tasks/' . $this->task->id)
                ->assertInputValue('#title', 'test task')
                ->screenshot('task_post_pressed');
        });
    }
}

setUp()メソッドは、テストのメソッドが実行される度に呼ばれるメソッドとなる。 ここでデータベースへ毎回データを挿入しておけば、テスト用のデータとして扱えることになる。

testShowDetail()testPost()で固定になっていたIDを、 setUp()メソッドで作成したレコードのIDを参照するように修正している。 ついでに、testPost()で正しく更新されることの確認もできるようになったので、 54行目にassertInputValue('#title', 'test task')を追加した。

再び、PHPUnitを実行してみよう。

$ vendor/bin/phpunit tests/Browser/TaskDetailTest.php 
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 11.16 seconds, Memory: 18.00MB

OK (2 tests, 4 assertions)

成功となった。うまくいくと気持ち良いものだ。 この気持ちのまま、ビューテストを修正してしまおう。

次はtests/Browser/TasksIndexTest.phpだ。





 






 

 

 
 
 
 
 
 
 
 


























 
 





<?php

namespace Tests\Browser;

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

class TasksIndexTest extends DuskTestCase
{
    use DatabaseMigrations;

    private $task;

    protected function setUp()
    {
        parent::setUp();
        $this->task = Task::create([
            'title' => 'テストタスク',
            'executed' => false,
        ]);
    }

    /**
     * Tasks Index Test.
     *
     * @throws \Exception
     * @throws \Throwable
     */
    public function testIndex()
    {
        $this->browse(function (Browser $browser) {
            $browser->visit('/tasks')
                ->assertSee('テストタスク')
                ->screenshot("tasks_index");
        });
    }

    /**
     * Index To Detail Test.
     *
     * @throws \Throwable
     */
    public function testIndexToDetail() {
        $this->browse(function (Browser $browser) {
            $browser->visit('/tasks')
                ->assertSeeLink('テストタスク')
                ->clickLink('テストタスク')
                ->waitForLocation('/tasks/' . $this->task->id, 1)
                ->assertPathIs('/tasks/' . $this->task->id)
                ->assertInputValue('#title', 'テストタスク');
        });
    }
}

修正の方針は、tests/Browser/TaskDetailTest.phpの時と同じだ。 今回はビューテスト全体をPHPUnitで実行してみよう。

$ vendor/bin/phpunit tests/Browser
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

....                                                                4 / 4 (100%)

Time: 15 seconds, Memory: 18.00MB

OK (4 tests, 8 assertions)

すべて成功だ!

これで、ビューテストについては既存のデータに独立した形となった。 今後テストを追加していく場合にも、そのテストで必要なデータを用意して実行する、 という流れで進めば良い。

コントローラのテストを既存データに依存させないように

では、続いてtests/Feature/TaskControllerTest.phpを修正してみよう。 こちらのテストでは、DatabaseTransactionsとしていたものを RefreshDatabaseに置き換えることになる。

ちなみに、DatabaseMigrationsはマイグレーションを行なうので、より正確にいうと「一旦テーブルを削除して作り直す」ことデータをリセットしている。 RefreshDatabaseは、テーブルは残した状態でレコードを削除する動作となる。












 

 

 
 
 
 
 
 
 
 




















 




























 


 

















 


 





<?php

namespace Tests\Feature;

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

class TaskControllerTest extends TestCase
{
    use RefreshDatabase;

    private $task;

    protected function setUp()
    {
        parent::setUp();
        $this->task = Task::create([
            'title' => 'テストタスク',
            'executed' => false,
        ]);
    }

    /**
     * Get All Tasks Path Test
     *
     * @return void
     */
    public function testGetAllTasksPath()
    {
        $response = $this->get('/tasks');

        $response->assertStatus(200);
    }

    /**
     * Get Task Detail Path Test
     *
     * @return void
     */
    public function testGetTaskPath()
    {
        $response = $this->get('/tasks/' . $this->task->id);

        $response->assertStatus(200);
    }

    /**
     * Get Task Detail Path Not Exists Test
     *
     * @return void
     */
    public function testGetTaskPathNotExists()
    {
        $response = $this->get('/tasks/0');

        $response->assertStatus(404);
    }

    /**
     * Put Task Detail Path Test
     *
     * @return void
     */
    public function testPutTaskPath()
    {
        $data = [
            'title' => 'test title',
        ];
        $this->assertDatabaseMissing('tasks', $data);

        $response = $this->put('/tasks/' . $this->task->id, $data);

        $response->assertStatus(302)
            ->assertRedirect('/tasks/' . $this->task->id);

        $this->assertDatabaseHas('tasks', $data);
    }

    /**
     * Put Task Detail Path Test 2
     *
     * @return void
     */
    public function testPutTaskPath2()
    {
        $data = [
            'title' => 'テストタスク2',
            'executed' => true,
        ];
        $this->assertDatabaseMissing('tasks', $data);

        $response = $this->put('/tasks/' . $this->task->id, $data);

        $response->assertStatus(302)
            ->assertRedirect('/tasks/' . $this->task->id);

        $this->assertDatabaseHas('tasks', $data);
    }
}

随分長くなったが、これもsetUp()でレコードを追加しそのIDを参照するように修正している。

では、これもテストを実行してみよう。 今回はtests/Feature以下だけをテストしてみる。

$ vendor/bin/phpunit tests/Feature
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

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

Time: 1.22 seconds, Memory: 18.00MB

OK (5 tests, 13 assertions)

これも成功となった。コントローラについても、既存のデータから独立したテストとなった。

全てのテストを既存データに依存させないように

では、最後のテストファイルに手をつけよう。

tests/Unit/TaskTest.phpの修正だ。まずは、現状確認のために実行してみよう。

$ vendor/bin/phpunit tests/Unit
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

FE..                                                                4 / 4 (100%)

Time: 536 ms, Memory: 14.00MB

There was 1 error:

1) Tests\Unit\TaskTest::testGetTaskDetail
ErrorException: Trying to get property 'title' of non-object

/Users/[ユーザ名]/task-manager/task-manager/tests/Unit/TaskTest.php:51

--

There was 1 failure:

1) Tests\Unit\TaskTest::testGetSeederTasks
Failed asserting that 0 matches expected 3.

/Users/[ユーザ名]/task-manager/task-manager/tests/Unit/TaskTest.php:25

ERRORS!
Tests: 4, Assertions: 5, Errors: 1, Failures: 1.

ふむ、エラーと失敗、となった。 正常に実行できなかったのは、testGetSeederTasks()メソッドとtestGetTaskDetail()だ。

これらはよくよく見ると、seederで投入した初期データが正しくデータベースに入っているのか、 という確認目的で実装したものだ。 どちらかといえばLaravelの仕様確認のためのテスト、という意味になる。

そもそもseederは正しく動くことはLaravelで保証されているもので、 テストとして残し続ける意味がもう無いだろう。 この二つのテストを削除してしまった方が、 「テストで確認すべきこと」を明確にできるはずだ。

つまり、「システムの仕様に沿う動作を確認するテスト」に集中すべきだ。

testGetTaskDetailNotExists()であれば、 「レコードが見つからなかった場合の振る舞い」のための確認として追加した。

testUpdateTask()は、 「レコードを更新する場合の振る舞い」のための確認として追加した。 さらにこちらには「レコードが存在した場合」も含まれる。

現時点では、この二つのメソッドをテストとして残せば十分だろう。

結果として、tests/Unit/TaskTest.phpは下記のようになる。

<?php

namespace Tests\Unit;

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

class TaskTest extends TestCase
{
    use RefreshDatabase;

    public function testGetTaskDetailNotExists()
    {
        $tasks = Task::find(0);
        $this->assertNull($tasks);
    }

    public function testUpdateTask()
    {
        $task = Task::create([
            'title' => 'test',
            'executed' => false,
        ]);

        $this->assertEquals('test', $task->title);
        $this->assertFalse($task->executed);

        $task->fill(['title' => 'テスト']);
        $task->save();

        $task2 = Task::find($task->id);
        $this->assertEquals('テスト', $task2->title);
    }
}

だいぶスッキリした。 では、実行しよう。

$ vendor/bin/phpunit tests/Unit
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 858 ms, Memory: 16.00MB

OK (2 tests, 4 assertions)

無事成功となった。

では、最後に全てのテストを一気にテストしてみよう。 各テストが既存データから独立しているので、 問題なく成功となるはずだ。

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

...........                                                       11 / 11 (100%)

Time: 22.69 seconds, Memory: 20.00MB

OK (11 tests, 25 assertions)

これで、本当にすべて成功だ!

だいぶ長くなったが、「既存データに依存しない」形へテストを書き換えることができた。 手間は多少かかったが「データに関する箇所」の修正に集中できたはずだ。 テストを書き続けていることで、方針の転換を行なう場合でもやるべきことが明確になっている。

本章のまとめ

この章では、「詳細画面でタスクを更新する」という機能を実装した。 また、seederで投入した初期データに依存していたテストを、 各テストで独立した形に修正も行なった。

テストを作る度にデータを考えて追加する手間は増えるのだが、 「今テストしたいこと」に集中できるようになる。

次は、「タスクの追加」にチャンレジだ。

Last Updated (JST): 7/7/2019, 2:32:08 PM