概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: I’m not testing those stupid views!
- 原文公開日: 2016/10/25
- 著者: Jeroen Weeink
- サイト: Crafting Ruby
Railsのビューは頭悪そうなぐらいシンプルに保つべし(翻訳)
Railsプロジェクトに長年携わっているうちに、最初は純真きわまりないビューだったのが、いつしかネストだらけの複雑怪奇なRubyコードと入り組んだHTMLを煮込んだようなものに変わり果てていました。こうなってしまうと、理解するのも大変なら改修するのも大変です。ビューの取るに足らないような部分をちょっぴり修正するだけでも、エンドツーエンドテストを辛抱強く書いてはバグがつぶされたかどうかを確認することになります。しかもこの種のテストはテストスイートの総実行時間に激しく影響するので、これ以上テストを増やしたくないと思うのが人情です。重要でも何でもないエッジケースならなおさらです。テストにはそうしたexpectationが全部詰まっているので、エンドツーエンドテストが増えれば増えるほどビューの改修が困難になります。
ビューの機能をどれだけ増やさずにいられるかが決め手です。ビューのロジックが複雑になればなるほど、テストも困難になります。複雑になれば、HTMLやらボタンのクリックやらCSSのセレクタなどなど、考えなければならないことがその分増えるからです。
この問題のシンプルな解決方法は、ロジックを別のクラスに移動することです。別クラスに切り出されたロジックならば単体テストも楽になります。単体テストは実行も早く、書くのも簡単です。
そこから抜粋したビューで考えてみましょう。少々込み入ったロジックも含まれています。
<div class="checkout">
<% if @order.line_items.count > 0 %>
<% if (@order.total - @order.paid) > 0 %>
<div class="outstanding-amount"><%= number_to_currency(@order.total - @order.paid) %></div>
<% end %>
<div class="all-the-line-items"></div>
<% if @order.cancelled_at.nil? && (@order.total - @order.paid) > 0 %>
<%= link_to "Cancel your order", cancel_order_path(@order) %>
<% end %>
<% else %>
Your order is empty!
<% end %>
</div>
ここで何が行われているのかを読み取るには、考える必要があります。特にif
条件を読み解くにはしばらく時間がかかるでしょう。
Railsプロジェクトの場合、ロジックの移動先の有力な候補はビューデコレータです。ロジックだけをここに移し、HTMLレンダリングから出来る限り切り離します。HTMLレンダリングのテストを書くのは、単純な戻り値テストを書くよりずっと面倒だからです。
1つのデコレータにすべてのロジックを詰め込まなければならないということはありません。1つのページだけを担当するデコレータを作成することも、ページの特定のセクションだけを担当するデコレータを作成するのも、ありです。1つのデコレータが、デコレータでない別のクラスのロジックを委譲しても構いません。デコレータについて詳しくは、Railsアンチパターン: Decoratorの肥大化に譲ります。
さて、上のビューのデコレータは以下のような感じになります。
class OrderDecorator < Draper::Decorator
delegate_all
def checkout_possible?
line_items.count > 0
end
def can_be_cancelled?
cancelled_at.nil? && !complete?
end
def complete?
unpaid == 0
end
def unpaid
total - paid
end
end
ロジックをデコレータに切り出してみると、かなり読みやすいビューになりました。
<div class="checkout">
<% if @order.checkout_possible? %>
<% unless @order.complete? %>
<div class="outstanding-amount">$ <%= number_to_currency(@order.unpaid) %></div>
<% end %>
<div class="all-the-line-items"></div>
<% if @order.can_be_cancelled? %>
<%= link_to "Cancel your order", cancel_order_path(@order) %>
<% end %>
<% else %>
Your order is empty!
<% end %>
</div>
このビューにはテストの必要なエッジケースがほとんどないことが完全に明らかです。誤りは肉眼で確認してもよいくらいです。複雑なエンドツーエンドテストを書く代わりにシンプルなテストを書けば済むので、時間も節約できます。テストスイートの実行も速くなるので、TDDサイクルが落ちることもさほどありません。ビューのロジックを排除すれば、油断なく見張り続ける必要もなくなるのでエネルギーの節約にもなります。