カテゴリー
サインイン 新規登録

間違いや改善の指摘

内容の技術的な誤り・誤字脱字やミスのご報告・解説やトピックの追記/改善のご要望は教材をさらに良くしていく上でとても貴重なご意見になります。

少しでも気になった点があれば、ご遠慮なく投稿いただけると幸いです🙏

実際には誤りではなく勘違いであっても、ご報告いただけることで教材のブラッシュアップにつながります。

質問ポリシー①

教材受講者みなさんのスムーズな問題解決のために、心がけていただきたいことがあります。

教材の内容に関する質問を投稿しましょう

教材の内容に関係のない質問や教材とは異なる環境・バージョンで進めている場合のエラーなど、教材に関係しない質問は推奨していないため回答できない場合がございます。

その場合、teratailなどの外部サイトを利用して質問することをおすすめします。教材の誤字脱字や追記・改善の要望は「文章の間違いや改善点の指摘」からお願いします。

1-4

テスト駆動でフィーチャーテストを書いてみる

テスト駆動でフィーチャーテストを書いてみる

フィーチャーテストでは、複数のクラスが連動するような処理の実行後の状態を確認します。ウェブサービスでは、一般的に、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.php
123456789101112131415161718
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() を使用してください。

php
1
Copied!
$response = $this->get('/lessons/1');

実際に HTTP 通信をするわけではなく、内部で Controller を生成し、プロダクションなら Illuminate\Http\Response オブジェクトを返す代わりにそのラッパークラスである \Illuminate\Testing\TestResponse クラスのインスタンスを返します。

次に、レスポンスコードをチェックするために、 TestResponse::assertStatus() を使います。

php
1
Copied!
$response->assertStatus(Response::HTTP_OK);

定数を使っていますが、 200 などの数値を指定してもいいでしょう(これらのコードはよほどのことがない限り変更されることはないので)。

続いて、返却されたレスポンス(HTMLドキュメントデータ)に含まれていなければならないデータが含まれているかどうかをチェックするために TestResponse::assertSee() を使います。

php
12
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.php
123456789101112
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(); }); }

Blueprint::id() は Laravel 7.0 で導入されたメソッドです。6.x の場合は代わりに $table->bigIncrements('id') を指定してください。

モデルファクトリを定義する際、適当な値を適当にセットしてよければ Faker を使います。

Faker は Laravel の機能ではなく、独立したパッケージです。

https://github.com/fzaninotto/Faker

文字列、数値、日付、時刻、などのプリミティブ型データをランダムに生成したり、配列からランダムに要素を取得したり、テストデータのようにデータの中身は特になんでも構わない、というようなシーンで役立ちます。

どんなプロバイダ(データの種類に応じてランダムデータを提供する仕組み)があるか興味ある方は、上記の GitHub リポジトリのドキュメントやソースコードを読んでみてください。

日本語の文字列がほしければ config/app.php の faker_localeja_JP に変更すると(すべてではないですが)日本語のランダム文字列が生成されるようになります。

config/app.php
1234567891011121314151617181920
-+
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.php
12345678910111213141516171819202122232425
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.php
123456789101112131415
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.php
123456789101112131415161718192021222324252627282930
++ + -+ + --++
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 テーブルにレコードが追加されます。

php
1
Copied!
$lesson = Lesson::factory()->create(['name' => '楽しいヨガレッスン']);

モデルファクトリについては、「3章:フィーチャーテストの作り方を学ぶ」でもう少し詳しく解説します。

続いてプロダクションコードを書きます。

まずはルーティングを定義します。

app/routes/web.php に以下のコードを追加してください。

Laravel 8.0 以降
Laravel 8 から ::class 形式で指定するようになったので、Controller クラスの use 文が必要です。先頭のセクションに以下の文を追加します。

php
1
Copied!
use App\Http\Controllers\LessonController;

ルート定義本体はクラス名とメソッド名のペアを配列で指定します。

php
1
Copied!
Route::get('/lessons/{lesson}', [LessonController::class, 'show'])->name('lessons.show');

Laravel 8.0 未満

php
1
Copied!
Route::get('/lessons/{lesson}', 'LessonController@show')->name('lessons.show');

次に Controller。空き状況はとりあえず残り枠数 0 で初期化し、そのまま View に渡すようにしておきます(最初のグリーンフェーズは、いちばん楽な方法で実装しましょう。テンポを大切に)。

app/Http/Controllers/LessonCotroller.php を以下のように編集してください。

app/Http/Controllers/LessonCotroller.php
123456789101112131415
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.php
1234
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.php
1234
Copied!
<h1>{{ $lesson->name }}</h1> <div> <span>空き状況: {{ $lesson->vacancyLevel->mark() }}</span> </div>

app/Http/Controllers/LessonCotroller.php から VacancyLevel を取り除きます。

app/Http/Controllers/LessonCotroller.php
1234
Copied!
public function show(Lesson $lesson) { return view('lesson.show', compact('lesson')); }

app/Models/Lesson.php にゲッターを定義します。 remainingCount() はひとまず 0 を返す状態で定義しておきます。

app/Models/Lesson.php
1234567891011121314
+++++++++
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.php
1234
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() メソッドの中身を実装してください。

それに伴い、

  1. App\Models\Reservation モデルを追加
  2. Reservation の HasMany を返す reservations() メソッドを追加
  3. モデルファクトリを使って reservations を追加するように testShow() を変更(テストデータはデータプロバイダで渡すようにする)

してください。

これもテスト駆動で、3, 1, 2 の順番でやってみてもいいでしょう。

3 に関してはヒントを書きます。完成形は下記のような感じになります。$capacity, $reservationCount, および $user の3つの変数をテストが通るように定義してください。 $capacity >= $reservationCount とする必要がありますので、そこだけご注意を。

php
12345
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 も、以下のようにこれらの値に応じて変化するようにしなければなりません。

php
1
Copied!
$response->assertSee("空き状況: {$expectedVacancyLevelMark}");

次節にこの課題の解答例を載せます。