教材の内容に関係のない質問や教材とは異なる環境・バージョンで進めている場合のエラーなど、教材に関係しない質問は推奨していないため回答できない場合がございます。
その場合、teratailなどの外部サイトを利用して質問することをおすすめします。教材の誤字脱字や追記・改善の要望は「文章の間違いや改善点の指摘」からお願いします。
フィーチャーテストでは、複数のクラスが連動するような処理の実行後の状態を確認します。ウェブサービスでは、一般的に、Controller を対象とし、HTTP リクエストを入力値、レスポンスを出力値として検査します。
ユニットテストのときと同様、クラス名やインターフェイスを決めます。
クラス名は LessonController、URLは、
GET /lessons/{id}
としておきましょう。
決まったらテストクラスを作ります。
console Copied!# php artisan make:test Http/Controllers/LessonControllerTest
tests/Feature/Http/Controllers/LessonControllerTest.php を開き、最小限のテストを書きます。
tests/Feature/Http/Controllers/LessonControllerTest.php123456789101112131415161718 Copied!<?php
namespace Tests\Feature\Http\Controllers;
use Illuminate\Http\Response;
use Tests\TestCase;
class LessonControllerTest extends TestCase
{
public function testShow()
{
$response = $this->get('/lessons/1');
$response->assertStatus(Response::HTTP_OK);
$response->assertSee('楽しいヨガレッスン');
$response->assertSee('×');
}
}
1行ずつ解説します。
まず最初に、テストしたい URL へアクセスするために TestCase::get()
を呼びます。POST リクエストの場合は TestCase::post()
を使用してください。
php1 Copied!$response = $this->get('/lessons/1');
実際に HTTP 通信をするわけではなく、内部で Controller を生成し、プロダクションなら Illuminate\Http\Response
オブジェクトを返す代わりにそのラッパークラスである \Illuminate\Testing\TestResponse
クラスのインスタンスを返します。
次に、レスポンスコードをチェックするために、 TestResponse::assertStatus()
を使います。
php1 Copied!$response->assertStatus(Response::HTTP_OK);
定数を使っていますが、 200
などの数値を指定してもいいでしょう(これらのコードはよほどのことがない限り変更されることはないので)。
続いて、返却されたレスポンス(HTMLドキュメントデータ)に含まれていなければならないデータが含まれているかどうかをチェックするために TestResponse::assertSee()
を使います。
php12 Copied!$response->assertSee('楽しいヨガレッスン');
$response->assertSee('×');
assertSee()
は HTML タグも含めることができ、純粋にテキストだけをチェックするのであれば assertSeeText()
を使うこともできます。
テストを実行します。
console Copied!# php artisan test tests/Feature/Http/Controllers/LessonControllerTest.php
FAIL Feature\Http\Controllers\LessonControllerTest
✕ show
Tests: 1 failed
Expected status code 200 but received 404. Failed asserting that false is true.
at tests/Feature/Http/Controllers/LessonControllerTest.php:21
17| public function testShow()
18| {
19| $response = $this->get('/lessons/1');
20|
> 21| $response->assertStatus(Response::HTTP_OK);
22| $response->assertSee('楽しいヨガレッスン');
23| $response->assertSee('×');
24| }
25| }
正しく失敗しました。
レッスン情報はデータベースにある想定なので、まずはモデルとテーブル、それとモデルファクトリを作ります。
モデル生成時に -a
オプションを渡すと、マイグレーション、モデルファクトリ、シーダ、およびコントローラーを一緒に生成します(それぞれ -m
, -f
, -s
, -c
を個別に指定することもできます)。
console Copied!# php artisan make:model Models/Lesson -a
テーブル構造は 0-6 設計にあるテーブル定義を見ながら up メソッドを完成させます。
app/database/migrations/xxxxxx_create_lessons_table.php のupメソッドを以下のように編集してください(xxxxxxにはマイグレーションファイルを作成した日時が入ります)。
app/database/migrations/xxxxxx_create_lessons_table.php123456789101112 Copied!public function up()
{
Schema::create('lessons', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('coach_name');
$table->unsignedInteger('capacity');
$table->timestamp('start_at');
$table->timestamp('end_at');
$table->timestamps();
});
}
モデルファクトリを定義する際、適当な値を適当にセットしてよければ Faker を使います。
Faker は Laravel の機能ではなく、独立したパッケージです。
https://github.com/fzaninotto/Faker
文字列、数値、日付、時刻、などのプリミティブ型データをランダムに生成したり、配列からランダムに要素を取得したり、テストデータのようにデータの中身は特になんでも構わない、というようなシーンで役立ちます。
どんなプロバイダ(データの種類に応じてランダムデータを提供する仕組み)があるか興味ある方は、上記の GitHub リポジトリのドキュメントやソースコードを読んでみてください。
日本語の文字列がほしければ config/app.php の faker_locale
を ja_JP
に変更すると(すべてではないですが)日本語のランダム文字列が生成されるようになります。
config/app.php1234567891011121314151617181920 -+ Copied! # 中略
/*
|--------------------------------------------------------------------------
| Faker Locale
|--------------------------------------------------------------------------
|
| This locale will be used by the Faker PHP library when generating fake
| data for your database seeds. For example, this will be used to get
| localized telephone numbers, street address information and more.
|
*/
'faker_locale' => 'en_US',
'faker_locale' => 'ja_JP',
/*
|--------------------------------------------------------------------------
# 中略
app/database/factories/LessonFactory.php を以下のように編集してください。
Laravel 8.0 以降
app/database/factories/LessonFactory.php12345678910111213141516171819202122232425 Copied!<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class LessonFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' => $this->faker->name,
'coach_name' => $this->faker->name,
'capacity' => $this->faker->randomNumber(2),
'start_at' => $this->faker->dateTime,
'end_at' => $this->faker->dateTime,
];
}
}
Laravel 8.0 未満
app/database/factories/LessonFactory.php123456789101112131415 Copied!<?php
/** @var \Illuminate\Database\Eloquent\Factory $factory */
use App\Models\Lesson;
use Faker\Generator as Faker;
$factory->define(Lesson::class, function (Faker $faker) {
return [
'name' => $faker->name,
'coach_name' => $faker->name,
'capacity' => $faker->randomNumber(2),
'start_at' => $faker->dateTime,
'end_at' => $faker->dateTime,
];
});
これで準備が整ったので、テストコードを整備しつつ、プロダクションコードを書いていきます。
まずはテストコードを、データベースを使うように変更していきます。
app/tests/Feature/Http/Controllers/LessonControllerTest.php を以下のように編集してください。
app/tests/Feature/Http/Controllers/LessonControllerTest.php123456789101112131415161718192021222324252627282930 ++ + -+ + --++ Copied!<?php
namespace Tests\Feature\Http\Controllers;
use App\Models\Lesson;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Response;
use Tests\TestCase;
class LessonControllerTest extends TestCase
{
use RefreshDatabase;
public function testShow()
{
$response = $this->get('/lessons/1');
$lesson = Lesson::factory()->create(['name' => '楽しいヨガレッスン']);
$response = $this->get("/lessons/{$lesson->id}");
$response->assertStatus(Response::HTTP_OK);
$response->assertSee('楽しいヨガレッスン');
$response->assertSee('×');
$response->assertSee($lesson->name);
$response->assertSee('空き状況: ×');
}
}
RefreshDatabase トレイトは、「Laravel アプリケーションにおけるテストの基本的な考え方」でも紹介しましたが、テスト実行時にデータベースを再構築してくれます。
以下の行は、同じく「Laravel アプリケーションにおけるテストの基本的な考え方」で紹介した、モデルファクトリを使ってデータベースにテストで使用するための「適当な」データを投入する処理です。以下の処理が実行されると lessons
テーブルにレコードが追加されます。
php1 Copied!$lesson = Lesson::factory()->create(['name' => '楽しいヨガレッスン']);
モデルファクトリについては、「3章:フィーチャーテストの作り方を学ぶ」でもう少し詳しく解説します。
続いてプロダクションコードを書きます。
まずはルーティングを定義します。
app/routes/web.php に以下のコードを追加してください。
Laravel 8.0 以降
Laravel 8 から ::class
形式で指定するようになったので、Controller クラスの use 文が必要です。先頭のセクションに以下の文を追加します。
php1 Copied!use App\Http\Controllers\LessonController;
ルート定義本体はクラス名とメソッド名のペアを配列で指定します。
php1 Copied!Route::get('/lessons/{lesson}', [LessonController::class, 'show'])->name('lessons.show');
Laravel 8.0 未満
php1 Copied!Route::get('/lessons/{lesson}', 'LessonController@show')->name('lessons.show');
次に Controller。空き状況はとりあえず残り枠数 0 で初期化し、そのまま View に渡すようにしておきます(最初のグリーンフェーズは、いちばん楽な方法で実装しましょう。テンポを大切に)。
app/Http/Controllers/LessonCotroller.php を以下のように編集してください。
app/Http/Controllers/LessonCotroller.php123456789101112131415 Copied!<?php
namespace App\Http\Controllers;
use App\Models\Lesson;
use App\Models\VacancyLevel;
class LessonController extends Controller
{
public function show(Lesson $lesson)
{
$vacancyLevel = new VacancyLevel(0);
return view('lesson.show', compact('lesson', 'vacancyLevel'));
}
}
続いて View。resources/views/lesson/show.blade.php を作ります。ひとまずマークアップは適当でいいです。
resources/views/lesson/show.blade.php1234 Copied!<h1>{{ $lesson->name }}</h1>
<div>
<span>空き状況: {{ $vacancyLevel->mark() }}</span>
</div>
テストを実行します。
console Copied!# php artisan test tests/Feature/Http/Controllers/LessonControllerTest.php
PASS Feature\Http\Controllers\LessonControllerTest
✓ show
Tests: 1 passed
Time: 4.01s
空き状況レベルを、レッスンオブジェクトから取れるようにリファクタリングしていきます。将来的な完成形のイメージとしては、レッスンが予約可能最大数と現在の予約数を保持しており、その差が空き状況レベルの初期値となる感じです。
resources/views/lesson/show.blade.php にある $vacancyLevel
を $lesson->vacancyLevel
に変更します。
resources/views/lesson/show.blade.php1234 Copied!<h1>{{ $lesson->name }}</h1>
<div>
<span>空き状況: {{ $lesson->vacancyLevel->mark() }}</span>
</div>
app/Http/Controllers/LessonCotroller.php から VacancyLevel
を取り除きます。
app/Http/Controllers/LessonCotroller.php1234 Copied!public function show(Lesson $lesson)
{
return view('lesson.show', compact('lesson'));
}
app/Models/Lesson.php にゲッターを定義します。 remainingCount()
はひとまず 0 を返す状態で定義しておきます。
app/Models/Lesson.php1234567891011121314 +++++++++ Copied!# 中略
class Lesson extends Model
{
public function getVacancyLevelAttribute(): VacancyLevel
{
return new VacancyLevel($this->remainingCount());
}
private function remainingCount(): int
{
return 0;
}
}
View で
<span>空き状況: {{ $lesson->vacancyLevel->mark() }}</span>
としていたところを
<span>空き状況: {{ $lesson->vacancyLevel }}</span>
と書けるように、 app/Models/VacancyLevel.php に __toString()
メソッドを追加します。
app/Models/VacancyLevel.php1234 Copied!public function __toString()
{
return $this->mark();
}
明示的に mark()
を呼ぶか、暗黙的に文字列変換させるかは好みの問題だと思うので、お好きなほうを選んでいただければと思います。
では最後にテストが通るか確かめましょう。
console Copied!# php artisan test tests/Feature/Http/Controllers/LessonControllerTest.php
PASS Tests\Feature\Http\Controllers\LessonControllerTest
✓ show
Tests: 1 passed
Time: 3.68s
remainingCount()
メソッドの中身を実装してください。
それに伴い、
reservations()
メソッドを追加testShow()
を変更(テストデータはデータプロバイダで渡すようにする)してください。
これもテスト駆動で、3, 1, 2 の順番でやってみてもいいでしょう。
3 に関してはヒントを書きます。完成形は下記のような感じになります。$capacity
, $reservationCount
, および $user
の3つの変数をテストが通るように定義してください。 $capacity
>= $reservationCount
とする必要がありますので、そこだけご注意を。
php12345 Copied!$lesson = Lesson::factory()->create(['name' => '楽しいヨガレッスン', 'capacity' => $capacity]);
for ($i = 0; $i < $reservationCount; $i++) {
$user = User::factory()->create();
$lesson->reservations()->save(Reservation::factory()->make(['user_id' => $user]));
}
同じユーザーが同一のレッスンに予約を入れることができないので、ループ内で毎回 User を生成します。
$capacity
と $reservationCount
をデータプロバイダから提供すると、
$expectedVacancyLevelMark
も、以下のようにこれらの値に応じて変化するようにしなければなりません。
php1 Copied!$response->assertSee("空き状況: {$expectedVacancyLevelMark}");
次節にこの課題の解答例を載せます。