Railsのviewテストを書こう

## 挨拶

自称、Ruby on Rails フロントエンドエンジニア:joy_cat:のnaofumiです。X [@naofumi](https://x.com/naofumi)では色々勝手なことを書いていますが、最近Qiitaをやろうかなと思っています。

日々、下記のことを考えて開発したり、Xにポストしたりしています。

1. Railsのテスト戦略。Viewのテストも
1. シンプルだけどUXを犠牲にしないHotwireの書き方
1. ActiveRecordのあまり知られていない機能を使いこなして、シンプル・高機能・堅牢なコードを書くコツ
1. なるべくScaffoldの型を保ちつつ、複雑な処理を書く方法

でも、ちゃんと記事をまとめるまでの気力がまだないので、サクッと書けるものを中心に書いていきます。内容は薄めでも背景は意外と考えているので、コメントを残してもらえれば長文解答します！

ではいよいよ本題です！

## あなたは何のテストを書いているのか？

私が見てきた現場のほとんどでは以下のテストしか書かれていなかった。

1. Model spec
1. Request spec

あと、必要に応じて少しMailer specとかもあるではないだろうか？

でもRSpecをインストールした状態で`bin/rails g scaffold Hoge`などをやるとわかるように、RSpecはその他にHelper spec, Controller spec, Routing spec, System spec, View specを用意してくれる。

**そいつらを無視して本当に良いのか？**という疑問が当然湧いてくる

## どこのテストを書くべきか？

これについては[Martin Fowlerが以下のように述べている](https://martinfowler.com/bliki/SelfTestingCode.html)。

> 私たちの姿勢としては、ごく平凡なコードを除き、テストのないコードは壊れているという前提でいる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3507700/9e24407f-c4c6-ab26-9076-9e7c02eb7ee9.png)

私は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をテストする方法が書いてある。

1. [Viewのトップ階層](https://railsguides.jp/testing.html#ビューをテストする) – コントローラテストして、結果を`assert_select`等でチェックする
1. [View partialのテスト](https://railsguides.jp/testing.html#ビューのパーシャルをテストする) – `render [partialのpath]`の結果をやはり`assert_select`等でチェックする
1. [Helperのテスト](https://railsguides.jp/testing.html#ビューのパーシャルをテストする) – View Helperはただの関数として扱えるので、関数のユニットテストの感覚でテストする。

RSpecの場合は公式ドキュメントの["What tests should I write?"](https://github.com/rspec/rspec-rails?tab=readme-ov-file#what-tests-should-i-write)からリンクをたどり、view specsやhelper specsの書き方を調べると良い。Scaffoldも参考になる。

概ね、RSpecの方は細かいサポートがあり、特にviewのトップ階層も単独で(controllerを動かすことなく)テストできる。

要するにviewテストに関する公式ドキュメントは結構充実しているよ、ということだ。

## モックは大事

View spec, Helper specの場合、原則としてユニットテストになる。Controllerを動かさず、ERBテンプレートとHelperのみを動かすことによって、余計なところをなるべく動かさないテストを書く。

その場合、認証・認可のステートはモックによって作り上げることになることが多い。ここをある程度使いこなせていることが大切になる。以下を確認しておくことを強くお勧めする。

1. Rspec MocksのGithubページ https://github.com/rspec/rspec-mocks
1. 公式ドキュメント https://rspec.info/features/3-12/rspec-mocks/basics/

## モックとRubocopを組み合わせた時の注意点

RSpecでは[verifying doubles](https://rspec.info/features/3-12/rspec-mocks/verifying-doubles/)の使用が推奨されていて、使わないとRubocopはこの辺りで文句を言ってくる。でもRailsのviewはダイナミックにメソッドにレスポンスすることも多く(例えばDeviseとかで)、verifying_doublesで追いきれない時がある。この場合は[without_partial_double_verification](https://rspec.info/documentation/3.9/rspec-mocks/RSpec/Mocks/ExampleMethods.html#without_partial_double_verification-instance_method)を使ってエラーを回避する

## 実例

### アドミとしてログインしている場合にアドミemailをヘッダーに表示

この場合はDeviseの`current_admin`とか`admin_signed_in`をモックする。

```ruby
# テスト
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している。設計としてはまずいけど、まぁこう書けるよという例として。

```ruby
# ヘルパー

  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
```

```ruby
# テスト

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の書き方が変わった**。

1. viewでテストを書きたくないので、ロジックはなるべくviewに持ってこないようにした。ロジックがviewになければ、面倒なviewテストを書く必要がないので、設計時の判断としてロジックはなるべくModelで書くようになった
1. helperを使う場合も、ロジックを含める場合はなるべく単純な関数(viewのステートに依存しない関数)を使うようになった
1. ERBの中にはシンプルな条件分岐以外は書かないようになった

結果としてコードが改善されたと感じている。テストが面倒だからコードの設計を変えるというのはかなりよくやる。

なお別記事で、実際にテストの書きにくさを根拠にviewコードのリファクタリングを行ってみたので、[こちらも参照していただきたい](https://qiita.com/naofumik/items/61b9c3ff77d97192e332)。

## 最後に

以上、

1. 私の自動テストへのアプローチ
1. view/helperテストの必要性
1. view/helperテストをどうやって書くか

についてざっくりと紹介した。

モックは勉強しなければならないが、viewのテストが決して難しいものではない。これを理解していただけたなら私としては嬉しい。そして必要性も感じていただけたならなお嬉しい。

ちまたのコードのメンテナンス性が崩壊するのは、意外とview周りじゃないかなと個人的には思っていて(いやほんと、この辺りが酷いのを見てきた）、この辺りが上達できるとRailsでメンテナンス性の高いコードが書けるようになるのではないかと期待している。

ではまた次回。そして、取り上げて欲しい話題があったらコメントに書いてください！