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

[前編はこちらです](https://qiita.com/wantonbe/items/630ed0c3290b4da5bcd9)

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

# 準備

メッセージと画像を保存するテーブルの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のマニュアル](https://readouble.com/laravel/5.5/ja/eloquent.html#mass-assignment)にも書いてありますが、Eloquentモデルの`create`を使う場合はモデルへ`fillable`か`guarded`属性のどちらかを設定する必要があります。ですので、`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してくれる