メインコンテンツまでスキップ

歩くスケルトン: 外から 1 本通す

06 章と 07 章は内側から作りました。判定のロジックを固め、その外に境界を足しました。まだ HTTP からは呼べません。

この章は逆向きに進みます。外側の入り口にテストを置き、そこから内側へ降ります。通すのは 1 本だけです。機能を増やすのではなく、経路が最後までつながっていることを先に確かめます。

この進め方を walking skeleton と呼びます1。骨だけで、肉はまだありません。ただし歩きます。

この章で学ぶこと​

  • 外側から始めるときの最初の Red の置き方
  • 経路を 1 本だけ通すために、何を作り何を作らないか
  • 内側を先に作ってあることで何が変わるか
前提知識

この章で扱わないこと​

観点参照先
HTTP テストのアサーション一式、失敗パスの固定Pest でテストを書く
4 層構成と依存性逆転、層の切り方そのものLaravel での DDD アーキテクチャ
継続的インテグレーションでのテスト実行AWS 実践ガイド

ブラウザを動かす E2E テストは本ガイドの範囲外です。この章の「外側」は HTTP の入り口までを指します。

この章の開始時点のコード​

07 章までに、判定と 3 つの境界ができています。

final class ReserveRoom
{
public function __construct(
private ClockInterface $clock,
private ReservationRepository $repository,
private Notifier $notifier,
) {}

public function execute(RoomId $roomId, TimeRange $range, string $applicantEmail): Reservation
{
// 過去なら断る / 重なれば断る / 受け付けて保存し、通知する
}
}

ReservationRepository の実装は、まだ配列で動く Fake しかありません。時計と通知の実装も、テスト用のもの (固定の時計、通知のダブル) しかありません。

外側から始めるとは​

いま無いもののうち、この章の本文で作るのは、次の 3 つです。ほかに要る部品は、「内側は既にある」の節で数えます。

  1. HTTP のルート
  2. リクエストをユースケースの入力に変換するところ
  3. 本物のリポジトリ (データベースに書くもの)

内側から進める癖がついていると、3 から作りたくなります。データベースのテーブルを決め、リポジトリを実装し、最後にルートをつなぐ。

外側から進めると順序が逆になります。まず「叩いたら返ってくる」ことをテストにし、そのために足りないものだけを埋めます。

この順序の利点は、経路の断絶に早く気づけることです。内側から作ると、最後につなぐ段階で初めて「リクエストの形が合わない」「認証で弾かれる」といった問題が出ます。外側から通しておけば、その種の問題は最初の 1 本で出尽くします。

最初の受入テスト​

HTTP を叩くテストを書きます。このように、利用者に近い入り口から最後まで通して確かめるテストを、本ガイドでは受入テストと呼びます。

use Psr\Clock\ClockInterface;

test('予約を登録して 201 を返す', function () {
$this->instance(ClockInterface::class, new FixedClock(at('2026-09-21 09:00')));

$response = $this->postJson('/api/reservations', [
'room_id' => 'room-1',
'start' => '2026-10-01T13:00:00+09:00',
'end' => '2026-10-01T14:00:00+09:00',
'email' => 'taro.test@example.com',
]);

$response->assertCreated();
});

時計は 07 章の FixedClock に差し替え、コンテナに渡して固定します。実行日がいつでも、この予約は未来の時間帯として扱われます。

走らせると 404 で落ちます。ルートが無いからです。

これは 06 章で「仕様側の理由で落とす」と言ったのとは違う落ち方です。外側から始める場合、最初の Red は経路が存在しないことを指しています。この章が確かめたいのは経路そのものなので、この落ち方で構いません。

貫くために最小で埋める​

404 を消すためにルートを足します。API のルートは routes/api.php に書きます。このファイルは初期状態には無いので、php artisan install:api で作ります (このコマンドは Laravel Sanctum も入れます)。このファイルのルートには /api が自動で付きます。

Route::post('/reservations', ReserveRoomController::class);

次はルートを読み込むところで落ちます。ReserveRoomController というクラスが無いので、Laravel がルートを登録する時点で Invalid route action の例外を投げ、テストはリクエストを送る前に止まります。作ります。

final class ReserveRoomController
{
public function __construct(private ReserveRoom $useCase) {}

public function __invoke(ReserveRoomRequest $request): JsonResponse
{
$reservation = $this->useCase->execute(
new RoomId($request->string('room_id')->toString()),
new TimeRange(
new DateTimeImmutable($request->string('start')->toString()),
new DateTimeImmutable($request->string('end')->toString()),
),
$request->string('email')->toString(),
);

return response()->json(
['id' => $reservation->id->toString()],
Response::HTTP_CREATED,
);
}
}

コントローラの仕事は変換だけです。リクエストの文字列をドメインの型にし、戻ってきたものを JSON にする。判断は一切しません。判断は 07 章のユースケースに既にあります。

最後に、本物のリポジトリを 1 つだけ作ります。ここでも最小です。

final class EloquentReservationRepository implements ReservationRepository
{
public function findByRoom(RoomId $roomId): array
{
return ReservationRecord::where('room_id', $roomId->toString())
->get()
->map(fn (ReservationRecord $r) => $r->toDomain())
->all();
}

public function add(Reservation $reservation): void
{
ReservationRecord::create($reservation->toArray());
}
}

テストが緑になります。HTTP から入って、データベースまで届き、応答が返りました。

内側は既にある​

ここで足したものを数えます。本文で見せたのは、ルート 1 行、コントローラ 1 つ、リポジトリの実装 1 つです。動かすには、ほかに次の配線の部品が要ります。

  • 入力を受ける FormRequest (ReserveRoomRequest。make:request の雛形は authorize() が false を返すので true にする)
  • テーブルを作るマイグレーションと、Eloquent のモデル (ReservationRecord と、ドメインの型に戻す toDomain())
  • マイグレーションでテーブルを作り、テストごとの変更をトランザクションで巻き戻す RefreshDatabase
  • インターフェースを実装に結ぶ束縛 (時計・保存・通知の 3 つ) と、時計と通知の本番の実装
  • Reservation が持つ id と toArray()

どれも変換と配線で、判断のロジックは 1 行も書いていません。

これは 06 章と 07 章で先に作ってあったからです。外側から始めるといっても、内側が空のまま外だけ通すわけではありません。もし内側が無ければ、コントローラに判断を書きたくなります。そして一度書いたものは、後から動かすのが難しくなります。

外側から始める進め方は、内側をダブルで埋めながら降りていくこともできます。その場合、降りきった時点でダブルを本物に置き換えます。どちらでも構いません。避けたいのは、経路をつなぐ作業の途中で、判断を外側に書いてしまうことです。

何が確かめられたか​

この 1 本で確かめたことと、確かめていないことを分けます。

内容
確かめたルートが解決する。リクエストがドメインの型に変換できる。ユースケースが呼ばれる。データベースに書ける。201 が返る
確かめていない過去の時間帯を断ること。重なりを断ること。通知が送られること。バリデーションのエラー応答。認証と認可

「確かめていない」の行は、この後に足すテストの一覧です。骨が通っていることと、機能が揃っていることは別です。

重なりを断るところは 07 章で既に確かめました。ただしそれは Fake を使ったユースケースのテストです。HTTP から叩いたときに同じ結果になるかは、別のテストで確かめます。両方が要るのは、変換の層で取り違えが起きうるからです。

06 章との違い​

同じ題材を 2 方向から作りました。

06 章から 07 章08 章
始点判定のロジックHTTP の入り口
最初の Red が指すもの仕様を満たしていない経路がつながっていない
早く決まること仕様の境界、値の形入出力の形、層の責務
遅れること全体がつながるか細かい条件の判断

どちらが正しいということはありません。新しいシステムを始めるときは外側を 1 本通してから内側を作るのがよく使われる順序です。経路の不確実さを先に潰せるからです。既にある仕組みへ機能を足すなら、経路は通っているので内側から始まります。

第 3 部へ渡すもの​

第 3 部の React が関わる API の形を、ここで決めておきます。

項目内容第 3 部での扱い
一覧GET /api/rooms/{roomId}/reservations → 予約の配列 (部屋名と時間帯を含む)10 章が叩きます
登録POST /api/reservations → 201 と作成した予約の id09 章は送信前の検証までを扱い、送信そのものは範囲外です

一覧の GET はこの章では作っていません。POST と同じ手順 (受入テストを先に落とし、ルートと変換と永続化を最小で足す) で通せるので、練習として残します。

重なりで断る 409 と、入力不正の 422 も同じです。応答を決めるところまでが 08 章の範囲で、画面側でどう見せるかは本ガイドでは扱いません。

10 章は、この一覧の応答を前提にフロント側を書きます。09 章は通信の手前 (フォームの検証) までです。

まとめ​

  • 外側から始めるときの最初の Red は、経路が存在しないことを指します
  • 経路を通すために足すのは、ルート・変換・永続化と、それらをつなぐ配線の最小実装だけです。判断は足しません
  • 内側を先に作ってあると、コントローラは変換だけで済みます
  • 1 本通ったことは、機能が揃ったことを意味しません。確かめた範囲を書き出しておきます
  • 新しく始めるなら外から、既存に足すなら内からが、よく使われる順序です

次に読む​

Footnotes​

  1. walking skeleton は Alistair Cockburn による概念で、「端から端まで通る、ごく小さな実装」と定義されます。TDD の文脈では Steve Freeman と Nat Pryce の『Growing Object-Oriented Software, Guided by Tests』(2009) が同概念を引いて出発点に置いています。 ↩