TLDR;
簡単なWEBアプリケーションを書くときに最低限書いておいて良かったと思うテストの紹介
リポジトリは下記
https://github.com/katsuren/laravel-test
前提
Not SPA - JS が絡むものは NodeJS のテストフレームワーク使ったほうがよい
Not SPA でも JS が多い場合は dusk や codeception を使ったほうが良いと思う
Laravel についての知識、下記は知っているものとして解説する
factory、リレーションあたり
feature/unit テストについて
サンプルアプリケーションの概要
CDの管理アプリケーション作る
アーティストとそのアルバム、曲名を管理できるアプリケーション
簡単のためにログインは省く
テスト戦略的なもの
モデルが利用可能なこと、リレーションが正しいことをおさえる
基本的なリソースの CRUD のテストをおさえる
具体的には、各ルーティングがエラーで落ちていないことを確認する
また、各機能画面にてあるべき input があることを確認する (nameで確認)
その他、このテストでは RefreshDatabase を利用するので、.env.testing を用意して別DBでテストしたほうが良い。
さもなくばテストのたびにDBの中身がクリアされる。
M(モデル)のテスト
・マイグレーションを書く(テーブル定義)
・ファクトリーを書く(レコード生成の定義)
・ファクトリーが使えることを確認
これで最低限モデルの定義が間違っていないことが保証される
具体的なファイルは下記のようになる
# database/migrations/2019_03_04_123520_create_artists_table.php
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateArtistsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('artists', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('artists');
}
}
# app/Eloquents/Artist.php
<?php
namespace App\Eloquents;
use Illuminate\Database\Eloquent\Model;
class Artist extends Model
{
protected $guarded = [
'id',
];
}
# database/factories/ArtistFactory.php
<?php
use App\Eloquents\Artist;
use Illuminate\Support\Str;
use Faker\Generator as Faker;
$factory->define(Artist::class, function (Faker $faker) {
return [
'name' => $faker->name,
];
});
テストは下記
# tests/Unit/Eloquents/ArtistTest.php
<?php
namespace Tests\Unit\Eloquents;
use App\Eloquents\Artist;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ArtistTest extends TestCase
{
use RefreshDatabase;
public function testFactoryable()
{
$eloquent = app(Artist::class);
$this->assertEmpty($eloquent->get()); // 初期状態では空であることを確認
$entity = factory(Artist::class)->create(); // 先程作ったファクトリーでレコード生成
$this->assertNotEmpty($eloquent->get()); // 再度getしたら中身が空ではないことを確認し、ファクトリ可能であることを保証
}
}
M(モデル)のテスト2(リレーション)
アーティストは複数のアルバムを保持することができ、
アルバムはアーティストに所属している、ことを確認する
これをテストすることで、リレーションが保証される。
各モデルは下記のようになる
# app/Eloquents/Artist.php
<?php
class Artist extends Model
{
// 下記追加
public function albums()
{
return $this->hasMany(Album::class);
}
}
# app/Eloquents/Album.php
<?php
namespace App\Eloquents;
use Illuminate\Database\Eloquent\Model;
class Album extends Model
{
protected $guarded = [
'id',
];
public function artist()
{
return $this->belongsTo(Artist::class);
}
}
テストは次のようになる
# tests/Unit/Eloquents/ArtistTest.php
<?php
namespace Tests\Unit\Eloquents;
use App\Eloquents\Album;
use App\Eloquents\Artist;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ArtistTest extends TestCase
{
// 下記のテスト追加
public function testArtistHasManyAlbums()
{
$count = 5;
$artistEloquent = app(Artist::class);
$albumEloquent = app(Album::class);
$artist = factory(Artist::class)->create(); // アーティストを作成
$albums = factory(Album::class, $count)->create([
'artist_id' => $artist->id,
]); // アーティストに紐づくアルバムレコードを作成 (create の引数に指定するとその値でデータ作成される)
// refresh() で再度同じレコードを取得しなおし、リレーション先の件数が作成した件数と一致することを確認し、リレーションが問題ないことを保証
$this->assertEquals($count, count($artist->refresh()->albums));
}
}
# tests/Unit/Eloquents/AlbumTest.php
<?php
namespace Tests\Unit\Eloquents;
use App\Eloquents\Album;
use App\Eloquents\Artist;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AlbumTest extends TestCase
{
use RefreshDatabase;
public function testFactoryable()
{
$eloquent = app(Album::class);
$this->assertEmpty($eloquent->get());
$entity = factory(Album::class)->create();
$this->assertNotEmpty($eloquent->get());
}
public function testAlbumBelongsToArtist()
{
$albumEloquent = app(Album::class);
$artistEloquent = app(Artist::class);
$artist = factory(Artist::class)->create();
$album = factory(Album::class)->create([
'artist_id' => $artist->id,
]);
$this->assertNotEmpty($album->artist);
}
}
VC (ビュー・コントローラー)のテスト
具体的なソースコードは長くなってしまうので概要のリポジトリを参照のこと。
見た目は変わりやすいのでここではテスト対象にしない。(崩れやすいテスト) もし必要な場合は dusk などでスクリーンショット比較などをする。
簡単な CRUD コントローラーで、各ルーティングにアクセスして 4xx または 5xx が出ないことを確実にする。
リソースをそれぞれ簡単に説明すると
index = リソースの一覧表示の画面、検索フォームなどがある場合が多い
show = 指定のリソースの詳細表示画面、
create = 新規作成画面、フォームがあることを確認する
store = 新規作成処理(POST)
edit = リソースの更新設定画面、フォームがあることを確認する
update = 更新処理(PUT)
destroy = 削除処理(DELETE)
となる。
各画面の機能として、input の name="xxx" がすべて表示されることを assert することで必ず input があるということを保証する。
これにより、最低限入力フォームの存在とその処理が保証されることになる。
例えば ArtistsController は下記のような実装だとする
# app/Http/Controllers/ArtistsController.php
<?php
namespace App\Http\Controllers;
use App\Eloquents\Artist;
use App\Http\Requests\ArtistRequest;
class ArtistsController extends Controller
{
protected $artistEloquent;
public function __construct(Artist $artistEloquent)
{
$this->artistEloquent = $artistEloquent;
}
public function index()
{
$artists = $this->artistEloquent->pimp(request()->input('search', []))->get();
return view('artists.index')->with([
'artists' => $artists,
]);
}
public function show($id)
{
$artist = $this->artistEloquent->find($id);
return view('artists.show')->with([
'artist' => $artist,
]);
}
public function create()
{
return view('artists.edit')->with([
'isCreate' => true,
]);
}
public function store(ArtistRequest $request)
{
$artist = $this->artistEloquent->create($request->input('artist'));
return redirect('/artists/' . $artist->id)->with('flash_message', 'アーティストを作成しました');
}
public function edit($id)
{
$artist = $this->artistEloquent->find($id);
return view('artists.edit')->with([
'isCreate' => false,
'artist' => $artist,
]);
}
public function update(ArtistRequest $request, $id)
{
$artist = $this->artistEloquent->find($id);
$artist->fill($request->input('artist'));
$artist->save();
return redirect('/artists/' . $id)->with('flash_message', 'アーティストを更新しました');
}
public function destroy($id)
{
$artist = $this->artistEloquent->find($id);
$artist->delete();
return redirect('/artists')->with('flash_message', 'アーティストを削除しました');
}
}
それに対するテストは下記のように HTTP アクセスで確認する
基本的にHTMLの検査は文字列の検査しかできない、つまりdocumentの要素をリッチに検索するAPIは提供されておらず、その場合はduskを利用する。
簡素に変更がされないものを対象に検査する。具体的には下記の項目。
・200(HTTP OK)が返ること、またはリダイレクト(301 or 302)が返ること。
・フォームが存在する場合はそのアクションURLが存在すること、正しいこと。
・フォームが存在する場合はその項目(name=xxx)すべてが存在すること。
# tests/Feature/ArtistsControllerTest.php
<?php
namespace Tests\Feature;
use App\Eloquents\Artist;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
use Tests\TestCase;
class ArtistsControllerTest extends TestCase
{
use RefreshDatabase;
public function testCanSeeIndex()
{
$response = $this->get('/artists');
$response->assertOk() // インデックスにアクセスして 200 が返ることを確認
->assertSee('action="/artists"') // 検索フォームのアクションがあることを確認
->assertSee('name="search[name]"'); // search[name] という検索項目があることを確認
}
public function testCanSeeShow()
{
$artist = factory(Artist::class)->create();
$response = $this->get('/artists/' . $artist->id);
$response->assertOk()
->assertSee($artist->name); // 詳細ページでレコード固有の文字列が閲覧できることを確認
}
public function testCanCreate()
{
$response = $this->get('/artists/create');
$response->assertOk()
->assertSee('action="/artists"') // 作成フォームがあること
->assertSee('name="artist[name]"'); // 名前入力フォームがあること
$name = Str::random(10);
$response = $this->from('/artists/create')->post('/artists', [
'artist' => [
'name' => $name,
],
]); // アクションURLに対してポストしてリダイレクトされること
$response->assertRedirect();
$this->assertDatabaseHas('artists', ['name' => $name]); // DBにデータが作成されていることで新規作成処理を保証する
}
public function testCanUpdate()
{
$artist = factory(Artist::class)->create();
$response = $this->get('/artists/' . $artist->id . '/edit');
$response->assertOk()
->assertSee('action="/artists/' . $artist->id . '"') // 編集フォームがあること
->assertSee('name="artist[name]"'); // 名前入力フォームがあること
$name = Str::random(10);
$response = $this->from('/artists/' . $artist->id . '/edit')->put('/artists/' . $artist->id, [
'artist' => [
'name' => $name,
],
]); // アクションURLに対してポストしてリダイレクトされること
$response->assertRedirect();
$this->assertDatabaseHas('artists', ['name' => $name]); // レコードに変更した名前のレコードがあることで編集処理を保証する
}
public function testCanDelete()
{
$artist = factory(Artist::class)->create();
$this->assertDatabaseHas('artists', ['id' => $artist->id]); // テスト前にレコードがDBに存在することを確認
$response = $this->from('/artists')->delete('/artists/' . $artist->id);
$response->assertRedirect();
$this->assertDatabaseMissing('artists', ['id' => $artist->id]); // DELETEルートにアクセス後、DBからレコードがなくなっていることで削除処理を保証する
}
}
最後にまとめ
上記のものではもちろんテストが十分というわけではないが、最低限書いてあると嬉しい。レビューもテスト書かれてないよりはあったほうがいい。
あと、実際のアプリケーションではもちろんこのように単純ではない場合が多いので、必要に応じて書き足す必要がある。というかこのサンプルでも色々足りていないのは承知している。
簡単に書いたつもりだが意外と時間かかった。
しかし、各モデルがしっかり使えるものであることの保証と、各ルーティングにアクセス可能であることが証明できていれば、多少手荒な更新があってもなんとかやっていけるはず。テストどう書いていいかわからない、方針がたたないような初学者の人の助けになればと思う。