🐡

HotwireなRailsアプリケーションのRequest spec事情

2024/09/24に公開

厳密には弊プロダクトはHotwireではないのでタイトルは釣りかもしれません.
しかし、RailsでHTMLをレンダリングしているため、HTMLをRailsでレンダリングする場合のRSpec事情については同じことが言えるので色々書いていこうと思います。

この記事については、VueからRailsへの置き換えを担当してくれたメンバーから色々テスト方針について質問をもらったこともあり、その質問に対しての回答を改めてできればと考えております。

Railsの置き換えを進めている記事については下記をご覧ください。

https://zenn.dev/osiro/articles/c38f1018b2f10a

FeatureSpec及びSystemSpecについては、E2EテストサービスであるAutifyを利用していることもあって現在は利用をあまりしていない状態ですので、この記事での言及はしません。弊社でのAutifyの活用事例は下記記事をご覧ください。

https://zenn.dev/osiro/articles/caa350015dbac7

Request specでのテスト内容

RailsでHTMLをレンダリングしているということは、Request specでほぼ完全なHTMLが手に入るということです。
よって、JSを動かさない部分はHTMLだけでもテストを行うことが可能です。
簡単に文字列をチェックするテストも、NokogiriをはじめとしたHTMLパーサーを使えば細かく画面をテストすることも可能だと思います。

当然e2eほど実行環境は近くはありませんが、ちょうどユニットテストとE2Eテストの間で、よく見るテストトロフィーの図の「Integration Test」に近いことをできるのではないかと思っています。[1]

JSを含んだコードのテストはできないため、Request specでのテスト範囲を広げるためには複雑なビジネスロジックだったりを可能な限りフロントエンドではなくRailsなどのバックエンドに寄せていく必要があります。

Request specでHTMLをテストするのはすこしつらい

Request specで幾分かの画面に関わるテストをある程度カバーできるのはRequest specはE2Eよりも安定していることもあってRailsでHTMLをレンダリングしているHotwireの強みであると考えています。
しかし、Request specでHTMLをパースして検証するのは当たり前ですがJSONよりつらいのが難点で、レンダリングされている順番だったり正しいか検証するのは面倒です。
なので、簡単にHTMLの検証を行うカスタムマッチャーを準備しておくのがお勧めです。

Capbara::DSLをRequest specでincludeする

GitHubが提供しているViewComponentsではCapybara::DSLをincludeして、カスタムマッチャーを使えるようにしているようです。
同じようにCapybara::DSLをRequest specでも利用することで同じカスタムマッチャーを利用できるようになります。

config.include Capybara::RSpecMatchers, type: :request

上記をrails_helper.rbに記載すると、
have_selectorCapybara::DSLのマッチャーをRequest Spec上で利用することができます。

it "displays a list of posts" do
  expect(response.body).to have_selector('.post', count: 4)
end

他にも have_text などのCSSセレクターを利用したSpecを実装できます。

SpecHelperをNokogiriでつくる

Capybaraに依存したくない場合は、SpecHelperをNokogiriで作る選択も取れます。

RSpec::Matchers.define :nokogiri_selector do |selector, count: nil|
  match do |response_body|
    doc = Nokogiri::HTML(response_body)
    elements = doc.css(selector)

    # 件数の検証
    if count
      return false unless elements.size == count
    end

    true
  end

  failure_message do |response_body|
    message = "expected to find #{count || 'any'} elements matching '#{selector}'"
    if count
      message += ", but found #{Nokogiri::HTML(response_body).css(selector).size}"
    end
    message
  end
end
it "displays a list of posts" do
  expect(response.body).to nokogiri_selector('.post', count: 4)
end

このようにHTMLをパースするマッチャーを自作することで、HTMLの検証を行うことがちょっとだけ簡単になります。

時には割り切りも必要

Rails上でデータがレンダリングされている順番をテストしたい場合はおそらくHTMLに対して何かしらのセレクターが必要になるケースがあると思います。
このあたりの議論はフロントエンドのテストでも一定議論されている話題ではあるので、Request specでのHTML検証においても割り切ってしまい、テスト用のHTML属性をつけてしまうのが今はいいかなと思っています。

ViewSpecやHelperSpecなど、Request spec以外のテストで担保する

Request specで細かくテストを書くのではなく、HTMLをレンダリングする際に利用しているHelperやViewに対して網羅的にSpecを書くことで動作を担保するケースも取れると思います。
確かにViewやHelperが複雑なのにSpecが書かれていない現場もそれなりにはあるので、Request specがつらい場合、ViewSpecやHelperSpecを書いて担保してもよいのかと思いました。[2]
よりユニットテストに近い層のテストではあるので、テストの運用方針だったりはチーム内で一定議論が必要かと思います。

ViewについてのSpecだったりは下記の記事を参考にするのが良いかと思います。
https://qiita.com/naofumik/items/9136c830ed3b8a16b4e8
https://qiita.com/naofumik/items/61b9c3ff77d97192e332

JSのテストはどうする?

もちろんJSなしではWebアプリケーション開発を行うのは到底不可能なので、ここは素直にJestを使ったりでテストをするようにしています。
重要なビジネスロジックを使ったりはできるだけフロントエンドでは行わないようにして、どうしてもというシーンはフロントエンドライブラリに依存しない純粋関数で実装を行ってJestのテストを書くようにしてテストを行いやすくしています。

まとめ

昨今ではRailsがAPIに徹するプロダクトが多いのか、Hotwireなプロダクトでどのようにテストを運用しているかの情報がないので、Request specでの工夫だったりをまとめてみました。

もし参考になったりした部分やこんなことやっているよ!みたいな知見も、コメントやXでぜひ教えてください!

脚注
  1. 私自身はこれがHotwireで開発する1番の強みであると考えています ↩︎

  2. テストは重複しますが、重要な観点の場合はHelperSpecも書くし、Request specでも確認することがあります ↩︎

OSIRO テックブログ

Discussion