第3章 タスクの更新と完了
05.既存のデータに依存しないテストへ
PHPUnit
コマンドで実行
ビューテストをRefreshDatabase
とDatabaseMigrations
を使う前に、少し調整をしなければいけないことがある。
ビューテストはphp artisan dusk
で実行してきたのだが、
これでは(現在の.env
の設定においては)RefreshDatabase
やDatabaseMigrations
を使うことができない。
少々面倒な話だが、実行環境に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
で投入した初期データに依存していたテストを、
各テストで独立した形に修正も行なった。
テストを作る度にデータを考えて追加する手間は増えるのだが、 「今テストしたいこと」に集中できるようになる。
次は、「タスクの追加」にチャンレジだ。