さてようやく準備が整いましたのでデータベースを使ったテストを書いてみましょう。今回はメッセージと画像の保存する処理、取得する処理を例にしてテストを書いていきたいと思います。
準備
メッセージと画像を保存するテーブルのmigrationファイルをつくります。
$ php artisan make:model Models/Eloquent/Message --migration
$ php artisan make:model Models/Eloquent/MessageImage --migration
できあがったらテーブル定義を書いて行きましょう。今回はこんな感じになります。
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateMessagesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('messages', function (Blueprint $table) {
$table->increments('id');
$table->text('body');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('messages');
}
}
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateMessageImagesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('message_images', function (Blueprint $table) {
$table->increments('id');
$table->integer('message_id')->unsigned();
$table->string('path', 500);
$table->timestamps();
$table->foreign('message_id')
->references('id')->on('messages')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('message_images');
}
}
書きあがったらmigrationをします。migrationは実際の環境だけでなくテスト環境にもかけます。
$ php artisan migrate --env=testing
Migrating: 2018_12_01_062042_create_message_images_table
Migrated: 2018_12_01_062042_create_message_images_table
実際にテストを書く
準備ができたので実際にテストを書いていきます。まずはメッセージを保存する処理のテストと実装をしていきます。今回はModelの操作はServiceクラスに寄せようと思います。
保存処理のテスト
メッセージを保存できる
最初にテストを書きます。とりあえずシンプルに書いてみようとするとこういう感じになると思います。
<?php
namespace Tests\Services\Eloquent;
use App\Services\Eloquent\MessageService;
use App\Models\Eloquent\Message;
use Tests\TestCase;
class MessageServiceTest extends TestCase
{
public function testStoreMessage()
{
$service = new MessageService();
$service->store($body = 'テストメッセージです');
$this->assertCount(1, Message::all());
}
}
<?php
namespace App\Services\Eloquent;
use App\Models\Eloquent\Message;
class MessageService
{
public function store($body)
{
return Message::create(['body' => $body]);
}
}
実際に実行してみましょう。
$ ./vendor/bin/phpunit tests/Services/Eloquent/MessageServiceTest.php
E 1 / 1 (100%)
Time: 1.05 seconds, Memory: 12.00MB
There was 1 error:
1) Tests\Services\Eloquent\MessageServiceTest::testStoreMessage
Illuminate\Database\Eloquent\MassAssignmentException: body
エラーになりました。Laravelのマニュアルにも書いてありますが、Eloquentモデルのcreate
を使う場合はモデルへfillable
かguarded
属性のどちらかを設定する必要があります。ですので、Message
モデルに設定を入れて再度実行しましょう。
<?php
namespace App\Models\Eloquent;
use Illuminate\Database\Eloquent\Model;
class Message extends Model
{
protected $fillable = ['body'];
}
$ ./vendor/bin/phpunit tests/Services/Eloquent/MessageServiceTest.php
PHPUnit 6.5.13 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 1.16 seconds, Memory: 12.00MB
OK (1 test, 1 assertion)
うまくいきました。
メッセージと画像パスを一緒に登録できるようにする
続いて同じメソッドに画像パスを渡した場合はmessage_images
に保存するテストと実装を書いていきます。
use App\Models\Eloquent\MessageImage;
// (略)
public function testStoreMessageAndImage()
{
$service = new MessageService();
$service->store($body = 'テストメッセージ画像付き', $path = '/path/to/image');
$this->assertCount(1, Message::all(), 'メッセージが保存できてない');
$this->assertCount(1, MessageImage::all(), '画像が保存できてない');
}
use App\Models\Eloquent\MessageImage;
// (略)
public function messageImages()
{
return $this->hasMany('App\Models\Eloquent\MessageImage');
}
<?php
namespace App\Models\Eloquent;
use Illuminate\Database\Eloquent\Model;
use App\Models\Eloquent\Message;
class MessageImage extends Model
{
protected $fillable = ['path'];
}
public function store($body, $path = '')
{
$message = Message::create(['body' => $body]);
if ($path) {
$message->messageImages()->create(['path' => $path]);
}
return $message;
}
それではテストを実行してみましょう。
$ ./vendor/bin/phpunit tests/Services/Eloquent/MessageServiceTest.php
PHPUnit 6.5.13 by Sebastian Bergmann and contributors.
FF 2 / 2 (100%)
Time: 1.34 seconds, Memory: 12.00MB
There were 2 failures:
1) Tests\Services\Eloquent\MessageServiceTest::testStoreMessage
Failed asserting that actual size 2 matches expected size 1.
/app/chat/tests/Services/Eloquent/MessageServiceTest.php:17
2) Tests\Services\Eloquent\MessageServiceTest::testStoreMessageAndImage
メッセージが保存できてない
Failed asserting that actual size 3 matches expected size 1.
/app/chat/tests/Services/Eloquent/MessageServiceTest.php:25
FAILURES!
Tests: 2, Assertions: 2, Failures: 2.
2つとも失敗してしまいました。メッセージをみるとfindしたときにヒットしているレコード数が一致していないようです。実際にデータベースをみてみるとテストを実行した時に入れたレコードが残っていることがわかります。
mysql> SELECT * FROM messages;
+----+--------------------------------------+---------------------+---------------------+
| id | body | created_at | updated_at |
+----+--------------------------------------+---------------------+---------------------+
| 1 | テストメッセージです | 2018-12-01 11:50:52 | 2018-12-01 11:50:52 |
| 2 | テストメッセージです | 2018-12-01 12:31:30 | 2018-12-01 12:31:30 |
| 3 | テストメッセージ画像付き | 2018-12-01 12:31:31 | 2018-12-01 12:31:31 |
+----+--------------------------------------+---------------------+---------------------+
3 rows in set (0.00 sec)
これではテストを実行するたびにデータが残ってしまうのでテストがうまく行きません。とはいえ毎回消すのも面倒なのでいい感じにテストが終了したらデータベースのデータをなししたいです。
そういうことであればtearDown
時に全テーブルをDELETEすれば事足りそうですが、Laravelではテスト用に便利traitがあるのでそちらを使うようにします。今回はRollbackしたいのでDatabaseTransactions
を使います。このtraitは各テストを実行するときにトランザクションを開始して、テストが終了したらテストで入れたデータをRollbackしてくれます。早速ベースのテストケースクラスでuseしましょう。
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Foundation\Testing\DatabaseTransactions;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
use DatabaseTransactions;
}
準備ができたらデータを削除してテストを再実行します。
$ ./vendor/bin/phpunit tests/Services/Eloquent/MessageServiceTest.php
PHPUnit 6.5.13 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 1.29 seconds, Memory: 12.00MB
OK (2 tests, 3 assertions)
今度はうまくいきましたね!何度実行しても結果が変わらないのでテストが安定しているといえます。
クラス内でトランザクションをしている場合どうするか
storeの処理でトランザクションを貼った場合トランザクションがネストして効かないんじゃないかという疑問がでます。こちらも確認してみるとわかりますが実は大丈夫なんです。LaravelはマルチトランザクションができるのでテストではDatabaseTransactions
traitを有効にするだけでデータベースを初期状態に戻してくれます。
<?php
namespace App\Services\Eloquent;
use App\Models\Eloquent\Message;
use DB;
class MessageService
{
public function store($body, $path = '')
{
return DB::transaction(function () use ($body, $path) {
$message = Message::create(['body' => $body]);
if ($path) {
$message->messageImages()->create(['path' => $path]);
}
return $message;
});
}
}
この状態で2回テストを実行してもfailしないことがわかります。
$ ./vendor/bin/phpunit tests/Services/Eloquent/MessageServiceTest.php
PHPUnit 6.5.13 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 1.55 seconds, Memory: 12.00MB
OK (2 tests, 3 assertions)
$ ./vendor/bin/phpunit tests/Services/Eloquent/MessageServiceTest.php
PHPUnit 6.5.13 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 1.26 seconds, Memory: 12.00MB
OK (2 tests, 3 assertions)
データベース操作のコードのテストをやるのにtrait1つ読み込むだけで楽にできるのでいいですね!
まとめ
- データベースのテストをするときは初期状態に戻そう
- Laravelでは
DatabaseTransactions
traitをつかえば実行後にrollbackしてくれる