挨拶
自称、Ruby on Rails フロントエンドエンジニアのnaofumiです。X @naofumiでは色々勝手なことを書いていますが、最近Qiitaをやろうかなと思っています。
日々、下記のことを考えて開発したり、Xにポストしたりしています。
- Railsのテスト戦略。Viewのテストも
- シンプルだけどUXを犠牲にしないHotwireの書き方
- ActiveRecordのあまり知られていない機能を使いこなして、シンプル・高機能・堅牢なコードを書くコツ
- なるべくScaffoldの型を保ちつつ、複雑な処理を書く方法
でも、ちゃんと記事をまとめるまでの気力がまだないので、サクッと書けるものを中心に書いていきます。内容は薄めでも背景は意外と考えているので、コメントを残してもらえれば長文解答します!
ではいよいよ本題です!
あなたは何のテストを書いているのか?
私が見てきた現場のほとんどでは以下のテストしか書かれていなかった。
- Model spec
- Request spec
あと、必要に応じて少しMailer specとかもあるではないだろうか?
でもRSpecをインストールした状態でbin/rails g scaffold Hoge
などをやるとわかるように、RSpecはその他にHelper spec, Controller spec, Routing spec, System spec, View specを用意してくれる。
**そいつらを無視して本当に良いのか?**という疑問が当然湧いてくる
どこのテストを書くべきか?
これについてはMartin Fowlerが以下のように述べている。
私たちの姿勢としては、ごく平凡なコードを除き、テストのないコードは壊れているという前提でいる
私はMartin Fowlerの言葉を次のように解釈している。
誰が見ても誤解のしようがないほどロジックが追いやすいコード以外は、テストを書くべき
詳しくは別の機会に書きたいと思うが、はっきりと認識していただきたいのは、Model specとRequest specしかテストを書かないのはどう考えても不足。特にERB/Hotwireを書くならば、View specは少なくともポイントポイントでは絶対に必要であるということ。
(JSONを返す場合でも、認可状態によってレスポンスを変えるならあった方が良いと思う)
どういう時にView specが必要になってくるか?
例えば認証・認可の状態、あるいは注文の状態によってUIはいろいろ変化しなければならない。アカウントの一般ユーザに見せるボタン、アカウント管理者に見せるボタンは当然変わる。また注文直後の状態に表示するボタンやバッジ、未払い請求書がある時に表示するボタンやバッジなどは変化しなければならない。
こういうのは複数の条件分岐(if文とか)を、最悪の場合はネストされた状態で書くことが多い。結果としてロジックが追いにくい、複雑なコードをviewに書いてしまうことになる。
こういう場合はview specもしくはhelper specを書いて、複雑なロジックが正常に動作することを保証するべきだと私は思う。
View spec, Helper specの書き方
Rails GuideにはViewをテストする方法が書いてある。
-
Viewのトップ階層 – コントローラテストして、結果を
assert_select
等でチェックする -
View partialのテスト –
render [partialのpath]
の結果をやはりassert_select
等でチェックする - Helperのテスト – View Helperはただの関数として扱えるので、関数のユニットテストの感覚でテストする。
RSpecの場合は公式ドキュメントの"What tests should I write?"からリンクをたどり、view specsやhelper specsの書き方を調べると良い。Scaffoldも参考になる。
概ね、RSpecの方は細かいサポートがあり、特にviewのトップ階層も単独で(controllerを動かすことなく)テストできる。
要するにviewテストに関する公式ドキュメントは結構充実しているよ、ということだ。
モックは大事
View spec, Helper specの場合、原則としてユニットテストになる。Controllerを動かさず、ERBテンプレートとHelperのみを動かすことによって、余計なところをなるべく動かさないテストを書く。
その場合、認証・認可のステートはモックによって作り上げることになることが多い。ここをある程度使いこなせていることが大切になる。以下を確認しておくことを強くお勧めする。
- Rspec MocksのGithubページ https://github.com/rspec/rspec-mocks
- 公式ドキュメント https://rspec.info/features/3-12/rspec-mocks/basics/
モックとRubocopを組み合わせた時の注意点
RSpecではverifying doublesの使用が推奨されていて、使わないとRubocopはこの辺りで文句を言ってくる。でもRailsのviewはダイナミックにメソッドにレスポンスすることも多く(例えばDeviseとかで)、verifying_doublesで追いきれない時がある。この場合はwithout_partial_double_verificationを使ってエラーを回避する
実例
アドミとしてログインしている場合にアドミemailをヘッダーに表示
この場合はDeviseのcurrent_admin
とかadmin_signed_in
をモックする。
# テスト
require 'rails_helper'
RSpec.describe AdminHelper do
fixtures :admins
describe '#admin_sign_in_status_badge' do
context 'admin signed_inのとき' do
before do
# https://stackoverflow.com/questions/14426746/testing-devise-views-with-rspec/
without_partial_double_verification do
allow(view).to receive(:current_admin).and_return(admins(:admin))
allow(view).to receive(:admin_signed_in?).and_return(true)
end
end
it 'emailが正しく表示される' do
expect(helper.admin_sign_in_status_badge)
.to eql '<strong>admin@example.com</strong> としてサインイン中'
end
end
context 'admin signed_outのとき' do
before do
# https://stackoverflow.com/questions/14426746/testing-devise-views-with-rspec/
without_partial_double_verification do
allow(view).to receive(:admin_signed_in?).and_return(false)
end
end
it '空文字列が返ってくる' do
expect(helper.admin_sign_in_status_badge)
.to be_nil
end
end
end
end
現状の並べ替えのステートによって、並べ替え矢印の表示を変えるヘルパー
この場合はhelperが直接params
を参照しているので、これをモックしている。そしてlink_to
に渡される引数(classを含めて)をassertしている。設計としてはまずいけど、まぁこう書けるよという例として。
# ヘルパー
def ordering_arrow(param_name, direction, default: false, &block)
selected = (params[param_name] == direction.to_s) || (params[param_name].blank? && default)
html_options = selected ? {} : { style: 'color: #CCC;' }
link_to({ param_name.to_sym => direction }, html_options, &block)
end
# テスト
RSpec.describe OrderingHelper do
describe '#ordering_arrow' do
before { allow(view).to receive(:link_to) }
it 'params[:order] = nil, default true should not be grayed' do
allow(params).to receive(:[]).with('order').and_return(nil)
helper.ordering_arrow('order', :desc, default: true) { 'link text' }
expect(view).to have_received(:link_to).with({ order: :desc }, {})
end
it 'ordering_arrow :asc, params[:order] = asc should not be grayed' do
allow(params).to receive(:[]).with('order').and_return('asc')
helper.ordering_arrow('order', :asc, default: true) { 'link text' }
expect(view).to have_received(:link_to).with({ order: :asc }, {})
end
it 'ordering_arrow :desc, params[:order] = asc should be grayed' do
allow(params).to receive(:[]).with('order').and_return('asc')
helper.ordering_arrow('order', :desc, default: true) { 'link text' }
expect(view).to have_received(:link_to).with({ order: :desc }, { style: 'color: #CCC;' })
end
end
end
結果としてviewコードの書き方が改善される
viewテストは決して難しくないのだが、そうは言ってもDOMに対してCSSセレクターでassertするのは億劫だし、Mockもあまり書きたくない。だから自分の感覚としては、view/helperテストを書くようになったら、自分のviewの書き方が変わった。
- viewでテストを書きたくないので、ロジックはなるべくviewに持ってこないようにした。ロジックがviewになければ、面倒なviewテストを書く必要がないので、設計時の判断としてロジックはなるべくModelで書くようになった
- helperを使う場合も、ロジックを含める場合はなるべく単純な関数(viewのステートに依存しない関数)を使うようになった
- ERBの中にはシンプルな条件分岐以外は書かないようになった
結果としてコードが改善されたと感じている。テストが面倒だからコードの設計を変えるというのはかなりよくやる。
なお別記事で、実際にテストの書きにくさを根拠にviewコードのリファクタリングを行ってみたので、こちらも参照していただきたい。
最後に
以上、
- 私の自動テストへのアプローチ
- view/helperテストの必要性
- view/helperテストをどうやって書くか
についてざっくりと紹介した。
モックは勉強しなければならないが、viewのテストが決して難しいものではない。これを理解していただけたなら私としては嬉しい。そして必要性も感じていただけたならなお嬉しい。
ちまたのコードのメンテナンス性が崩壊するのは、意外とview周りじゃないかなと個人的には思っていて(いやほんと、この辺りが酷いのを見てきた)、この辺りが上達できるとRailsでメンテナンス性の高いコードが書けるようになるのではないかと期待している。
ではまた次回。そして、取り上げて欲しい話題があったらコメントに書いてください!