Skip to main content

歩くスケルトン: 外から 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): Reservation
{
// 過去なら断る / 重なれば断る / 受け付けて保存し、通知する
}
}

ReservationRepository の実装は、まだ配列で動く Fake しかありません。

外側から始めるとは

いま無いのは、次の 3 つです。

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

内側から進める癖がついていると、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 章で先に作ってあったからです。外側から始めるといっても、**内側が空のまま外だけ通すわけではありません。**もし内側が無ければ、コントローラに判断を書きたくなります。そして一度書いたものは、後から動かすのが難しくなります。

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

何が確かめられたか

この 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 と作成した予約の id09 章は送信前の検証までを扱い、送信そのものは範囲外です

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

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

09 章と 10 章は、この応答を前提にフロント側を書きます。

まとめ

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

次に読む

Footnotes

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