ViewComponent::Previewを使ったRSpecの速度改善
先日開催されたTokyuRuby会議16に参加し、「自作アプリにViewComponentを導入した」というタイトルで登壇しました。この記事ではその発表内容について記載します。
発表スライドはこちらです。
自作アプリについて
今回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を使った画面のテストを書く際の参考になれば幸いです。
株式会社ラグザイア(luxiar.com)の技術広報ブログです。 ラグザイアはRuby on RailsとC#に特化した町田の受託開発企業です。フルリモートでの開発を積極的に推進しており、全国からの参加を可能にしています。柔軟な働き方で最新のソフトウェアソリューションを提供します。
Discussion