Posted at

Laravelアプリで開発テスト2 - データベースのテスト編(後編) -

前編はこちらです

さてようやく準備が整いましたのでデータベースを使ったテストを書いてみましょう。今回はメッセージと画像の保存する処理、取得する処理を例にしてテストを書いていきたいと思います。


準備

メッセージと画像を保存するテーブルのmigrationファイルをつくります。

$ php artisan make:model Models/Eloquent/Message --migration

$ php artisan make:model Models/Eloquent/MessageImage --migration

できあがったらテーブル定義を書いて行きましょう。今回はこんな感じになります。


database/migrations/2018_12_01_062014_create_messages_table.php

<?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');
}
}



database/migrations/2018_12_01_062042_create_message_images_table.php

<?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クラスに寄せようと思います。


保存処理のテスト


メッセージを保存できる

最初にテストを書きます。とりあえずシンプルに書いてみようとするとこういう感じになると思います。


tests/Services/Eloquent/MessageServiceTest.php

<?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());
}
}



app/Services/Eloquent/MessageService.php

<?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を使う場合はモデルへfillableguarded属性のどちらかを設定する必要があります。ですので、Messageモデルに設定を入れて再度実行しましょう。


app/Models/Eloquent/Message.php

<?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に保存するテストと実装を書いていきます。


tests/Services/Eloquent/MessageServiceTest.php

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(), '画像が保存できてない');
}



app/Models/Eloquent/Message.php

use App\Models\Eloquent\MessageImage;

// (略)

public function messageImages()
{
return $this->hasMany('App\Models\Eloquent\MessageImage');
}



app/Models/Eloquent/MessageImage.php

<?php

namespace App\Models\Eloquent;

use Illuminate\Database\Eloquent\Model;
use App\Models\Eloquent\Message;

class MessageImage extends Model
{
protected $fillable = ['path'];
}



app/Services/Eloquent/MessageService.php

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しましょう。


tests/TestCase.php

<?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を有効にするだけでデータベースを初期状態に戻してくれます。


app/Services/Eloquent/MessageService.php

<?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してくれる