← 前へ: Phase 4.3 | 目次 | 次へ: Phase 4.5 →
実行前に共通セットアップと各Phaseの確認手順を確認してください。演習コードはwork/で前のPhaseから引き継ぎます。
このフェーズでは、Phase 4.3で導入した Controller / Action 構造の上に、render / redirectを TDD(テスト駆動開発)で導入します。
Phase 4.3までで、Router から Controller の Action を呼び出せるようになりました。
しかし現状では、多くの Action が次のように直接 Rack レスポンスを返しているはずです。
[200, { "content-type" => "text/html" }, ["<h1>Hello</h1>"]]このままでは、
- Action のたびに同じ構造を書く必要がある
- HTTPレスポンスの詳細がアプリケーションコードに漏れ出る
- 「何を返したいか」より「どう返すか」に意識が向いてしまう
- redirect のような表現が不自然になる
という問題が発生します。
このフェーズでは、それを解決するために、
Action からは「意味」でレスポンスを表現し、 Controller 側で HTTP レスポンスを組み立てる
という構造を導入します。
なお、このフェーズでも必ず RED → GREEN → REFACTOR の流れで進めてください。
以下を説明できる状態になること:
- render は何を抽象化しているのか
- redirect は HTTP 的に何をしているのか
- なぜ Action から Rack 配列を直接返さない方がよいのか
- ControllerBase にレスポンス生成の責務を持たせる意味
- Controller から
render_textを呼べる - Controller から
render_htmlを呼べる - Controller から
redirect_toを呼べる - Action が Rack レスポンス配列を直接返さなくても動く
- 二重 render / redirect を防げる
- TDD でレスポンス生成の仕様を積み上げられる
- Phase 4.3(Controller / Action のTDD実装)完了
- Minitest を使えること
このフェーズでは、以下を必ず守ってください。
最終的には、以下のような Action を書ける状態を目指します。
class UsersController < ControllerBase
def index
render_text "Users Index"
end
def show
render_html "<h1>User Detail</h1>"
end
def create
redirect_to "/users"
end
endただし、最初からここを目指して一気に実装してはいけません。 必ず小さな振る舞いごとにテストを書いて進めてください。
まずは最小の render として、plain text を返す render_text を導入します。
def test_action_can_render_text
router = Router.new
router.get "/hello", to: "pages#hello"
status, headers, body = router.call(env_for("/hello"))
assert_equal 200, status
assert_equal "text/plain", headers["content-type"]
assert_equal ["Hello"], body
endclass PagesController < ControllerBase
def hello
render_text "Hello"
end
endrender_textがまだ存在しない- 失敗してよい
👉 これが RED
ControllerBaseにrender_textを追加するtext/plainを返す- ステータス 200 を返す
- ボディに文字列を入れる
- Action の戻り値として返すのか
- Controller 内部にレスポンス状態を持たせるのか
最初は不格好でも構いません。 まずはテストを通すことを優先してください。
render_text が導入できたら、次は Action の責務を明確にします。
def test_action_does_not_need_to_return_rack_response_directly
controller = PagesController.new(env_for("/hello"))
response = controller.dispatch(:hello)
assert_equal 200, response[0]
assert_equal "text/plain", response[1]["content-type"]
assert_equal ["Hello"], response[2]
endclass PagesController < ControllerBase
def hello
render_text "Hello"
nil
end
enddispatchのような経路が必要か考える- Action 実行後にレスポンスを組み立てる場所を定める
- まだ Action 実行とレスポンス生成の責務が整理されていないはず
👉 RED
dispatch(action_name)のような仕組みを導入する- Action 実行後に Controller の内部状態から Rack レスポンスを生成する
Action の中では
render_text "Hello"だけ書けばよく、Rack 配列は Controller 側が責任を持つようにすること
次に HTML を返す render を導入します。
def test_action_can_render_html
router = Router.new
router.get "/html", to: "pages#html"
status, headers, body = router.call(env_for("/html"))
assert_equal 200, status
assert_equal "text/html", headers["content-type"]
assert_equal ["<h1>Hello</h1>"], body
endclass PagesController < ControllerBase
def html
render_html "<h1>Hello</h1>"
end
endrender_htmlがまだないので失敗するはず
👉 RED
render_htmlを追加するcontent-typeをtext/htmlにする
render_textと共通化できる部分はどこか- 内部的に共通の
renderにまとめるべきか
まだ大きな抽象化は不要です。 重複が見えてから整理してください。
render を使うと、200以外のレスポンスも返したくなります。
def test_render_text_can_accept_status
router = Router.new
router.get "/not_found", to: "pages#not_found"
status, headers, body = router.call(env_for("/not_found"))
assert_equal 404, status
assert_equal "text/plain", headers["content-type"]
assert_equal ["Not Found"], body
endclass PagesController < ControllerBase
def not_found
render_text "Not Found", status: 404
end
endstatus:オプションがまだないなら失敗するはず
👉 RED
status:を受け取れるようにする- 指定がなければ 200
- 指定があればその値を使う
render は単なる文字列返却ではなく、
HTTP レスポンスの組み立てを抽象化していること
次に redirect を導入します。
def test_action_can_redirect
router = Router.new
router.post "/users", to: "users#create"
status, headers, body = router.call(env_for("/users", method: "POST"))
assert_equal 302, status
assert_equal "/users", headers["location"]
assert_equal [], body
endclass UsersController < ControllerBase
def create
redirect_to "/users"
end
endredirect_toがまだ存在しないはず
👉 RED
redirect_to(path)を追加する- ステータス 302
locationヘッダ- ボディは空配列でもよい
- redirect は render と何が違うか
- redirect は body を返すのではなく「移動先を通知する」こと
実務では 302 以外のリダイレクトもあります。
def test_redirect_to_can_accept_status
controller = UsersController.new(env_for("/users"))
response = controller.dispatch(:moved)
assert_equal 301, response[0]
assert_equal "/new_users", response[1]["location"]
endclass UsersController < ControllerBase
def moved
redirect_to "/new_users", status: 301
end
endstatus:オプションがまだなければ失敗する
👉 RED
status:を受け取れるようにする- デフォルトは 302
- 指定されたらその値を使う
1つの Action で複数回 render / redirect されると、レスポンスが不定になります。
def test_raises_error_when_render_called_twice
controller = DoubleRenderController.new(env_for("/double"))
assert_raises RuntimeError do
controller.dispatch(:index)
end
endclass DoubleRenderController < ControllerBase
def index
render_text "A"
render_text "B"
end
end- まだ二重 render を防いでいなければ失敗しないはず
👉 RED
- render または redirect 済みかどうかを管理する
- 2回目以降は例外を発生させる
- どのタイミングでフラグを立てるか
- render と redirect の両方に共通の判定が必要か
何も render / redirect しない Action をどう扱うかを、テストで先に決めてください。
- エラーにする
- 空レスポンスにする
- 暫定で 200 / empty body にする
def test_raises_error_when_nothing_rendered
controller = EmptyController.new(env_for("/empty"))
assert_raises RuntimeError do
controller.dispatch(:index)
end
endclass EmptyController < ControllerBase
def index
end
endここは「正解が1つ」ではありません。どの振る舞いにするかを先に決め、テストで固定することが重要です。
- Step 15 で決めた仕様に従って実装する
- なぜその仕様にしたのか説明できること
ここまで進むと、内部で次のような重複が見えてくるはずです。
- ステータスの設定
- ヘッダの設定
- body の設定
- 二重 render の判定
重複を減らし、責務を整理してください。
response_built?ensure_not_rendered!set_responsebuild_response
- Action は「何を返したいか」に集中できる
- ControllerBase は「どう HTTP レスポンスを組み立てるか」を担当する
以下のような使い方をテストから追加する:
render_json({ name: "Alice" })content-type: application/json- body は JSON 文字列
以下のような使い方をテストから追加する:
render_text "ok", headers: { "x-test" => "true" }- 空でもよいか
- 最低限のメッセージを返すべきか
- ブラウザ以外のクライアントではどう考えるか
最低限、以下を含むこと:
render_textが動くrender_htmlが動く- render に
status:を渡せる redirect_toが動く- redirect に
status:を渡せる - 二重 render / redirect が防がれる
- 何も render しない場合の仕様がテストで固定されている
ControllerBase- サンプル Controller
以下について説明してください:
- render は何を抽象化しているか
- redirect は HTTP 的に何をしているか
- Action から Rack 配列を直接返さない利点は何か
- 何も render しない場合の仕様をどう決めたか
- テストによって設計がどう変わったか
- RED → GREEN → REFACTOR を守れているか
- テストが振る舞いの仕様として機能しているか
- ControllerBase に責務が適切に集約されているか
- Action から HTTP の詳細が隠蔽されているか
- 二重 render / redirect を正しく防げているか
- 過剰設計になっていないか
- HTTPステータスコードの意味の確認
- redirect のHTTP仕様の確認
- テスト粒度の相談
- 責務分離の壁打ち
- 実装の生成
- 完成コードの出力
- 解答の丸写し
→ まずは render_text 1つから始めること
→ render 導入の意味が薄れる
→ location ヘッダとステータスコードが本質
→ フレームワークとして重要な責務
→ テストで固定すること
このフェーズで得るべき理解は、
フレームワークの仕事は、 HTTP の詳細を隠しながらも、HTTP との対応関係を失わせないこと
ということです。
render / redirect は単なる便利メソッドではありません。 それは、Action から見た「意味」と、HTTP レスポンスという「具体」の橋渡しです。
ここまで来ると次の問題が自然に見えてきます。
- HTML を Action の中に文字列で書くのがつらい
- 表示とロジックを分離したい
- データをビューに渡したい
次のフェーズでは、これを解決するために
View / Template
を TDD で導入します。
このフェーズでは、完成形に近い便利メソッドを作ることが目的ではありません。
大切なのは、
- どの振る舞いを先に固定したか
- どこで痛みを感じたか
- どの責務を ControllerBase に寄せたか
を、自分の手で設計しながら理解することです。