歩くスケルトン: 外から 1 本通す
06 章と 07 章は内側から作りました。判定のロジックを固め、その外に境界を足しました。まだ HTTP からは呼べません。
この章は逆向きに進みます。**外側の入り口にテストを置き、そこから内側へ降ります。**通すのは 1 本だけです。機能を増やすのではなく、経路が最後までつながっていることを先に確かめます。
この進め方を walking skeleton と呼びます1。骨だけで、肉はまだありません。ただし歩きます。
この章で学ぶこと
- 外側から始めるときの最初の Red の置き方
- 経路を 1 本だけ通すために、何を作り何を作らないか
- 内側を先に作ってあることで何が変わるか
06 ドメインロジックから始める と 07 境界を差し替える で作ったものを使います。
この章で扱わないこと
| 観点 | 参照先 |
|---|---|
| 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): Reservation
{
// 過去なら断る / 重なれば断る / 受け付けて保存し、通知する
}
}
ReservationRepository の実装は、まだ配列で動く Fakeフェイク (Fake)本物と同じ働きをする簡易実装のテストダブル。配列で動くリポジトリのように、書いたものを後から読める。 しかありません。
外側から始めるとは
いま無いのは、次の 3 つです。
- HTTP のルート
- リクエストをユースケースの入力に変換するところ
- 本物のリポジトリ (データベースに書くもの)
内側から進める癖がついていると、3 から作りたくなります。データベースのテーブルを決め、リポジトリを実装し、最後にルートをつなぐ。
外側から進めると順序が逆になります。まず「叩いたら返ってくる」ことをテストにし、そのために足りないものだけを埋めます。
この順序の利点は、経路の断絶に早く気づけることです。内側から作ると、最後につなぐ段階で初めて「リクエストの形が合わない」「認証で弾かれる」といった問題が出ます。外側から通しておけば、その種の問題は最初の 1 本で出尽くします。
最初の受入テスト
HTTP を叩くテストを書きます。
test('予約を登録して 201 を返す', function () {
$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',
]);
$response->assertCreated();
});
この日付は実行時より未来である必要があります。実行日に左右されない形にするなら、07 章の FixedClock をテストのときだけ差し替えます。
走らせると 404 で落ちます。ルートが無いからです。
これは 06 章で「仕様側の理由で落とす」と言ったのとは違う落ち方です。外側から始める場合、最初の Red は経路が存在しないことを指しています。それでよいのです。この章が確かめたいのは経路そのものだからです。
貫くために最小で埋める
404 を消すためにルートを足します。
Route::post('/api/reservations', ReserveRoomController::class);
次は 500 で落ちます。コントローラが無いからです。作ります。
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()),
),
);
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 つ。判断のロジックは 1 行も書いていません。
これは 06 章と 07 章で先に作ってあったからです。外側から始めるといっても、**内側が空のまま外だけ通すわけではありません。**もし内側が無ければ、コントローラに判断を書きたくなります。そして一度書いたものは、後から動かすのが難しくなります。
外側から始める進め方は、内側をダブルテストダブル (Test Double)テストのために本物の代わりへ差し込むオブジェクトの総称。Dummy・スタブ・スパイ・モック・フェイクの 5 つに分けて呼ぶ。で埋めながら降りていくこともできます。その場合、降りきった時点でダブルを本物に置き換えます。どちらでも構いません。避けたいのは、経路をつなぐ作業の途中で、判断を外側に書いてしまうことです。
何が確かめられたか
この 1 本で確かめたことと、確かめていないことを分けます。
| 内容 | |
|---|---|
| 確かめた | ルートが解決する。リクエストがドメインの型に変換できる。ユースケースが呼ばれる。データベースに書ける。201 が返る |
| 確かめていない | 過去の時間帯を断ること。重なりを断ること。通知が送られること。バリデーションのエラー応答。認証と認可 |
右側は、この後に足すテストの一覧です。骨が通っていることと、機能が揃っていることは別です。
重なりを断るところは 07 章で既に確かめました。ただしそれは Fake を使ったユースケースのテストです。HTTP から叩いたときに同じ結果になるかは、別のテストで確かめます。両方が要るのは、変換の層で取り違えが起きうるからです。
06 章との違い
同じ題材を 2 方向から作りました。
| 06 章から 07 章 | 08 章 | |
|---|---|---|
| 始点 | 判定のロジック | HTTP の入り口 |
| 最初の Red が指すもの | 仕様を満たしていない | 経路がつながっていない |
| 早く決まること | 仕様の境界、値の形 | 入出力の形、層の責務 |
| 遅れること | 全体がつながるか | 細かい条件の判断 |
どちらが正しいということはありません。新しいシステムを始めるときは外側を 1 本通してから内側を作るのがよく使われる順序です。経路の不確実さを先に潰せるからです。既にある仕組みへ機能を足すなら、経路は通っているので内側から始まります。
第 3 部へ渡すもの
ここで作った API を、第 3 部の React から叩きます。形を決めておきます。
| 項目 | 内容 | 第 3 部での扱い |
|---|---|---|
| 一覧 | GET /api/rooms/{roomId}/reservations → 予約の配列 (部屋名と時間帯を含む) | 10 章が叩きます |
| 登録 | POST /api/reservations → 201 と作成した予約の id | 09 章は送信前の検証までを扱い、送信そのものは範囲外です |
一覧の GET はこの章では作っていません。POST と同じ手順 (受入テストを先に落とし、ルートと変換と永続化を最小で足す) で通せるので、練習として残します。
重なりで断る 409 と、入力不正の 422 も同じです。応答を決めるところまでが 08 章の範囲で、画面側でどう見せるかは本ガイドでは扱いません。
09 章と 10 章は、この応答を前提にフロント側を書きます。
まとめ
- 外側から始めるときの最初の Red は、経路が存在しないことを指します
- 経路を通すために足すのは、ルート・変換・永続化の最小実装だけです。判断は足しません
- 内側を先に作ってあると、コントローラは変換だけで済みます
- 1 本通ったことは、機能が揃ったことを意味しません。確かめた範囲を書き出しておきます
- 新しく始めるなら外から、既存に足すなら内からが、よく使われる順序です
次に読む
- 09 振る舞いをテストする — ここで決めた API を叩く側を作ります
- Pest でテストを書く — HTTP テストのアサーションと失敗パスの固定