⌛

ViewComponent::Previewを使ったRSpecの速度改善

に公開

先日開催されたTokyuRuby会議16に参加し、「自作アプリにViewComponentを導入した」というタイトルで登壇しました。この記事ではその発表内容について記載します。
発表スライドはこちらです。
https://speakerdeck.com/siso25/zi-zuo-apuriniviewcomponentwodao-ru-sita

自作アプリについて

今回ViewComponentを導入した自作アプリは RouletteTalk というトークテーマルーレット作成アプリケーションです。トークテーマと話す人を登録して、ルーレットで抽選ができます。
Ruby on RailsおよびHotwireで実装しており、テストはRSpecを使用しています。

ViewComponent導入の背景

RouletteTalkでは、システムテストがテストケース数の割に遅いという課題がありました。
ルーレット作成処理の部分はシンプルなCRUDになっており、細かいテストケースはユニットテストで書くことができたため、実行時間が遅くなることはありませんでした。
しかし、ルーレットの表示や抽選、アニメーションなどの処理に関してはStimulus(JavaScript)を使って実装しているため、該当箇所のテストをシステムテストでカバーしなければなりませんでした。特に「スタート」ボタンクリック時は3秒ほどのアニメーションが入るため、以下のようにwaitでアニメーションの終了を待つ必要があり、結果としてルーレットを動かすテストを追加する度に3秒ずつテスト時間が伸びるという状況でした。

it 'トークテーマ数を超えた回数ルーレットを実行したら選択状態をリセットする', js: true do
  visit roulette_path(@roulette)
  click_button 'スタート'
  result_text = find "[data-rotate-target='resultText']", wait: 10 # waitでアニメーションの終了を待つ

ViewComponentとは

ViewComponentは、RailsでUIコンポーネントを実装するためのgemです。
app/componentsフォルダ内にRubyのクラスが記述されたrbファイルと、erbやslimなどのテンプレートファイルを作成することで、任意のViewファイルで使用することができます。

# app/components/example_component.rb
class ExampleComponent < ViewComponent::Base
  def initialize(name:)
    @name = name
  end
end
<%# app/components/example_component.html.erb %>
<h1>Hello, <%= @name %>!</h1>

コンポーネント使用時は以下のように記述します。

<%# app/views/example/index.html.erb %>
<%= render(ExampleComponent.new(name: "siso")) %>

ViewComponent::Preview

今回は、ViewComponentの中でもPreviewという機能を使うことでテストを改善できるのではないかと考えました。Previewは、規定のURLにアクセスすることで作成したコンポーネントをプレビューできる機能で、本来は開発者向けの確認用途ですが、以下の点からテスト用のエントリーポイントとして使用することにしました。

  • JavaScriptが動作する状態でコンポーネントをプレビューできる
  • URLパラメータ経由で任意の値を渡すことができる

Previewの使用方法

テストフォルダ内にプレビューファイルを作成し、以下のように記述します。
サーバー起動後、/rails/view_components/example_component/with_defaultにアクセスすることでコンポーネントのプレビューを確認できます。
また、/rails/view_components/example_component/with_name?name=sisoのようにURLパラメータを設定することで値を渡すこともできます。

# test/components/previews/example_component_preview.rb
class ExampleComponentPreview < ViewComponent::Preview
  def with_default
    render(ExampleComponent.new(name: "siso"))
  end

  # 引数に定義した'name'をURLパラメータで渡すことができる
  def with_name(name:)
    render(ExampleComponent.new(name: name))
  end
end

実装方法

ViewComponent::Previewを使うことでURLパラメータ経由で値を渡すことができるため、アニメーションの時間を外から渡すことで、テスト時のみルーレットが回転する時間を0秒にすることにしました。
以下に実装の内容を記載します。
※説明のため、実際のコードよりも簡略化しています。

Stimulus

まず、StimulusのコントローラーにValuesを追加し、アニメーションの実行時間を受け取れるようにします。

// app/javascript/controllers/rotate_controller.js
export default class extends Controller
  static values = {
    // ルーレットが回転する時間
    spinDuration: Number,
  }

  // スタートボタンクリック時の処理
  start() {
    // 渡って来た値を使ってアニメーションの実行時間を設定する
    const animation = this.rotate(this.spinDuration)

Component

次に、パーシャルで実装していた箇所をViewComponentで再実装します。
元々Slimを使用していたため、テンプレートファイルもSlimで実装しています。

# app/components/roulette_component.rb
class RouletteComponent < ViewComponent::Base
  def initialize(spin_duration:)
    @spin_duration = spin_duration
  end
end
/ app/components/roulette_component.html.slim
.relative[
  data-controller="rotate"
  data-rotate-spin-duration-value=@spin_duration

Preview

次に、ViewComponent::Previewのファイルを作成します。
URLパラメータでアニメーションの実行時間を渡せるように、引数としてspin_durationを定義します。
※本アプリではRSpecを使用しているため、specディレクトリ内にファイルを作成しています。
 RSpecでPreviewを使う場合はこちらの設定が必要です。

# spec/components/previews/roulette_component_preview.rb
class RouletteComponentPreview < ViewComponent::Preview
  # URLパラメータ "spin_duration" を受け取れるようにする
  def with_spin_duration(spin_duration:)
    render RouletteComponent.new(spin_duration: spin_duration)
  end
end

RSpec

最後にシステムスペックを修正します。
visitのURLをプレビューのURLに変更し、URLパラメータでアニメーションの実行時間を0秒に設定します。アニメーションが0秒になることによりfindの待機時間で十分になるため、waitを削除します。
また、プレビューを使ったテストはspec/system/componentsディレクトリにテストファイルを新規作成し、元のシステムテストから該当箇所を移行しました。

# spec/system/components/roulette_component_spec.rb
it 'トークテーマ数を超えた回数ルーレットを実行したら選択状態をリセットする', js: true do
  # プレビューのURLにアクセス
  visit '/rails/view_components/roulette_component/with_spin_duration?spin_duration=0'
  click_button 'スタート'
  result_text = find "[data-rotate-target='resultText']" # wait 10 を削除

実行時間の比較

今回の修正前後で全件テストを10回実行した平均が以下になります。
修正後の方はコンポーネント単位の単体テストも追加したので件数としては増えていますが、実行時間は元の2/3程度に短縮できました。

修正前 修正後
テスト件数 44件 64件
実行時間(秒) 36.963 22.875

まとめ

ViewComponent::Previewを使うことで、Stimulusの動作をテスト時に変更できるようになり、テストの実行時間を短縮することができました。
また、コンポーネント単位のシステムテストと元々のシステムテストでファイルが分かれたことにより、コンポーネント単位はStimulusメイン、元々のシステムテストファイルはCRUDを含めた画面全体のテストというようにスコープが明確になったのは、想定していないメリットでした。
Stimulusを使った画面のテストを書く際の参考になれば幸いです。

ラグザイア

Discussion