1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RSpecの基本のキホン!(システムスペック編)

Posted at

システムスペック

システムスペックとは

  • モデルやコントローラが他のモデルやコントローラとうまく連携するかをテストする(統合テスト)
  • 実際のユーザーがアプリケーションとどのようにやり取りするかをテストする
  • Rails 5.1 以降ではシステムテスト( system test ) という名前で、このタイプのテストがセットアップ時にデフォルトでサポートされている。

システムスペックで使用する gem

ブラウザの操作をシミュレートするために Capybara を使用する。

  • Capybara を使うとリンクをクリックしたり、Web フォームを入力したり、画面の表示を検証 したりすることができる。
  • Rails 5.1 以降であれば Capybara はすでにインストールされている。( system test で使用するため)
  • Capybara には Rails の開発環境で実行可能なジェネ レータは用意されていないため、Capybara が追加されているのはテスト環境のみ
Gemfile
group :test do
  gem 'capybara'
  .
  .
  .
end

システムスペックの作成

作成コマンド
$ rails g rspec:system projects
初期状態のspec/system/projects_spec.rb
require 'rails_helper'

RSpec.describe 'Projects', type: :system do
  before do
    driven_by(:rack_test)
  end

  pending "add some scenarios (or delete) #{__FILE__}"
end

上記の状態から、Capybara では click_link や fill_in 、visit といった理解しやすいメソッド(後述)が提供されていて、アプリケーションで 必要な機能のシナリオを以下のように書くことができる。

spec/system/projects_spec.rb
require 'rails_helper'

RSpec.describe "Projects", type: :system do
  before do
    driven_by(:rack_test)
  end

  # ユーザーは新しいプロジェクトを作成する
  scenario "user creates a new project" do # scenarioはitへ置き換えることも可能
    user = FactoryBot.create(:user)

    visit root_path
    click_link "Sign in"
    fill_in "Email", with: user.email
    fill_in "Password", with: user.password
    click_button "Log in"

    expect {
      click_link "New Project"
      fill_in "Name", with: "Test Project"
      fill_in "Description", with: "Trying out Capybara"
      click_button "Create Project"

      expect(page).to have_content "Project was successfully created"
      expect(page).to have_content "Test Project"
      expect(page).to have_content "Owner: #{user.name}"
    }.to change(user.projects, :count).by(1)
  end
end

click_button を使うと、起動されたアクションが完了する前に次の処理へ移ってしまうことがある。そこで、上記テストのように click_button を実行した expect{} の内部で最低でも 1 個以上のエクスペクテーションを実行し、処理の完了を待つようにするのが良い。

システムスペックの特徴

ユーザーインターフェースを無視して、パラメータを直接コントローラのメソッドに送信するコントローラースペックと異なり、上記で作成したシステムスペックは以下のようなユーザー操作のステップをシミュレートしている。

  • ユーザーでログインする
  • ログインしたユーザーが新しいプロジェクトを作成する
  • 作成したプロジェクトと処理成功のメッセージが表示される

1 アクションをテストするコントローラスペックとは異なり、このステップは以下の複数のコントローラ・アクションを一つのスペックでカバーする。

  • home#index
  • sessions#new
  • projects#index
  • projects#new
  • projects#create

Capybara のメソッド

以下のサンプルコードは Capybara の DSL が提供しているその他のメソッドの使用例。

scenario "works with all kinds of HTML elements" do
  # ページを開く
  visit "/fake/page"

  # リンクまたはボタンのラベルをクリックする
  click_on "A link or button label"

  # チェックボックスのラベルをチェックする
  check "A checkbox label"

  # チェックボックスのラベルのチェックを外す
  uncheck "A checkbox label"

  # ラジオボタンのラベルを選択する
  choose "A radio button label"

  # セレクトメニューからオプションを選択する
  select "An option", from: "A select menu"

  # ファイルアップロードのラベルでファイルを添付する
  attach_file "A file upload label", "/some/file/in/my/test/suite.gif"

  # 指定した CSS に一致する要素が存在することを検証する
  expect(page).to have_css "h2#subheading"

  # 指定したセレクタに一致する要素が存在することを検証する
  expect(page).to have_selector "ul li"

  # 現在のパスが指定されたパスであることを検証する
  expect(page).to have_current_path "/projects/new"
end

セレクタのスコープを制限する

Capybara の within を使って ページの一部分に含まれる要素を操作し, そのスコープを制限することができる。

<!-- click here!ボタンが2つある -->
<div id="node">
  <a href="http://nodejs.org">click here!</a>
</div>
<div id="rails">
<a href="http://rubyonrails.org">click here!</a>
</div>

上記のような HTML で、id = railsのリンクを選択したい場合、以下のように書くことができる。

# idを指定し、スコープを制限する
within "#rails" do
  click_link "click here!"
end

このようにすることで、id = railsのリンクを選択することができる。

Capybara の find メソッド

find メソッドは、ページの要素を検索し、その要素を取り出すことができる。

find メソッドの使用例
language = find_field("Programming language").value
expect(language).to eq "Ruby"

find("#fine_print").find("#disclaimer").click
find_button("Publish").click

システムスペックのデバッグ

Capybara のコンソール出力を読めば、どこでテストが失敗したのか調査することができる。
しかし、以下のようなテストは、ユーザーがログインしていないことが原因でテストは失敗するが、出力結果からは原因の一部しかわからない。

# ゲストがプロジェクトを追加する
  scenario "guest adds a project" do
    visit projects_path
    click_link "New Project"
  end
デバッグ出力
Failures:
  1) Projects guest adds a project
     Failure/Error: click_link "New Project"
     Capybara::ElementNotFound:
       Unable to find link "New Project" # ページに要求されたリンクがないことのみがわかる
    # 残りのスタックトレースは省略 ...

このような時に実際の画面を確認して原因をデバッグしたい。
driven_by メソッドで :rack_test を指定した場合、Capybara は ヘッドレスブラウザ( UI を持たないブラウザ)を使ってテストを実行するため、処理ステップを一つずつ目で確認することはできないが、デバッグ用のメソッドを挿入することで Rails がブラウザに返した HTML を見ることはできる。

デバッグ用のメソッドを挿入する
scenario "guest adds a project" do
  visit projects_path
  save_and_open_page # ここでページを保存して確認
  click_link "New Project"
end
デバッグ出力
File saved to
  /Users/asumner/code/examples/projects/tmp/capybara/
  capybara-201702142134493032685652.html.
Please install the launchy gem to open the file automatically.
  guest adds a project (FAILED - 1)

保存されたファイルをブラウザで開くと、実際の画面を確認することができる。(今回であれば、リダイレクトされてログイン画面が表示されていることがわかる)

また、コンソールにも出力されているが以下の gem をインストールすることで、自動でファイルを開くことができる。

Gemfile
group :test do
  gem 'launchy'
end

ブラウザを起動する必要がない場合や、ブラウザを起動できないコンテナ環境などでは 代わりに save_page メソッドを使うこともできる。このメソッドを使うと HTML ファイルが tmp/capybara に保存され、ブラウザは起動しない。

JavaScript を使った操作をテスト

ここまで Capybara はシンプルなブラウザシミュレータ(つまりドライバ)の:rack_test を使って、テストに書かれたタスクを実行していた。
:rack_test は速くて信頼性が高いが、JavaScript を使った操作をテストすることはできない。

このような場合、selenium-webdrivergem を使うことで、ブラウザを起動して JavaScript を使った操作をテストすることができる。
この gem は Rails 5.1 以降の Rails にはデフォルトでインストールされていて、Capybara でもデフォルトの JavaScript ドライバになっている。

ドライバの共通設定

使用するドライバは driven_by メソッドを使ってテストごとに変更することができるが、システム全体の共通設定として有効化するには以下のような手順を踏む。

1. spec/support ディレクトリを有効化

以下の行をコメント解除することで、Rspec 関連の設定ファイルを spec/support ディレクトリに配置して読み込むようにできる。

spec/rails_helper.rb
Rails.root.glob('spec/support/**/*.rb').sort.each { |f| require f }

2. ドライバの共通設定をするファイルを作成

ここではブラウザを使った基本的なテストでは高速なRack::Testドライバを使い、より 複雑なブラウザ操作が必要な場合は JavaScript が実行可能なドライバ(ここではselenium- webdriverChrome)を設定するようにしている。
このほかに Chrome とやりとりするインターフェースになる ChromeDriver が必要になるが、最新版の selenium-webdriver を使用すると自動的に適切なバージョンの ChromeDriver を ダウンロードしてくれるため、特別な設定は不要。

spec/support/capybara.rb
RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :rack_test
  end

  # js: true を指定することで、JavaScript を使った操作をテストすることができる。
  config.before(:each, type: :system, js: true) do
    driven_by :selenium_chrome
  end
end

3. 実際のテストへタグを付与する

spec/system/tasks_spec.rb
require 'rails_helper'

RSpec.describe "Tasks", type: :system do
  # ユーザーがタスクの状態を切り替える
  scenario "user toggles a task", js: true do # タグを付与してJSを有効化
    user = FactoryBot.create(:user)
    project = FactoryBot.create(
      :project,
      name: "RSpec tutorial",
      owner: user
    )
    task = project.tasks.create!(name: "Finish RSpec tutorial")

    visit root_path
    click_link "Sign in"
    fill_in "Email", with: user.email
    fill_in "Password", with: user.password
    click_button "Log in"

    click_link "RSpec tutorial"
    check "Finish RSpec tutorial"

    expect(page).to have_css "label#task_#{task.id}.completed"
    expect(task.reload).to be_completed

    uncheck "Finish RSpec tutorial"
    expect(page).to_not have_css "label#task_#{task.id}.completed"
    expect(task.reload).to_not be_completed
  end
end

この状態でテストを実行すると Chrome のウィンドウが新しく立ち上がり、実際にブラウザで JS を使った操作が実行される。

JavaScript を使った操作をテストする際の注意点

JavaScript を実行するテストと、Selenium を使うテストのデメリットとしてテストの実行時間の遅さが挙げられる。
JavaScript ドライバはだんだん速くなっているので、いつかRack::Testと同等のスピードで実行できるようになるかもしれないが、現状では必要なときにだけ、テスト上で JavaScript を有効にすることが推奨される。

ヘッドレスドライバを使用する

テストの実行中にブラウザのウィンドウが開くのはあまり望ましくないケースがよくある。
たとえば、GitHub Actions や Travis CI、Jenkins のような 継続的インテグレーション(CI) 環境で実行する場合、先ほど作ったテストは CLI(コマンドラインインターフェース)上で実行する必要がある。
しかし、CLI 上では新しいウィンドウを開くことはできない。
こういった要件に対応するため、Capybara は ヘッドレスドライバを使用できる。

以下のようにドライバを変更することで、Chrome のウィンドウが表示されることなくテストが完了する。

spec/support/capybara.rb
config.before(:each, type: :system, js: true) do
  driven_by :selenium_chrome_headless
end

ページの変化の完了を待ちたい場合

デフォルトでは Capybara はボタン要素の出現やページの変化の際にそれらが表示するまで 2 秒待機する。2 秒待っても表示されなければ諦める。
以下の設定でこの待ち時間を変更することができる。(spec/supportディレクトリを有効化していない場合は、spec/rails_helper.rb によって読み込まれる場所に配置)

spec/support/capybara.rb
# 10秒待つ設定
Capybara.default_max_wait_time = 10

しかし、この 変更はテストスイートの実行がさらに遅くなる原因になるかもしれないので注意。もしこの設定を変えたいと思ったら、テストコード上で必要に応じてその都度 using_wait_time を使うこともできる。

# 本当に遅い処理を実行する
scenario "runs a really slow process" do
  using_wait_time(15) do
    # テストを実行する
  end
end

実行時間の待機にsleepを使用することはアンチパターンとなるので避けるべき。
sleepは待機時間が固定のため、処理が完了しても待機時間が終了せず不必要に長いテスト時間となってしまう。

スクリーンショットでデバッグする

JavaScript ドライバ(selenium-webdriver)を使用するテストでは、take_screenshotメソッドを使用することで、テストの実行中にスクリーンショットを撮ることができる。
スクリーンショット画像はデフォルトではtmp/capybaraディレクトリに保存される。
また、Railsの標準機能としてシステムスペック失敗時にはスクリーンショットが自動的に保存される。

フィーチャースペック

システムスペックが導入される以前はフィーチャースペックと呼ばれるRSpec Railsgem 独自の機能を使って統合テストを書いていた。
相違点としては以下の通り。

  • spec/system ではなく spec/features にファイルを保存する
  • describe メソッドではなく feature メソッドを使う
  • type: オプションに :system ではなく :feature を指定する
  • let や let! のエイリアスとして given や given! が使える
  • before のエイリアスとして background が使える
  • スクリーンショットを撮る場合、save_screenshot は使えるが、take_screenshot は使えない
  • テストが失敗してもスクリーンショットは自動的に保存されない(明示的にsave_screenshotメソッドを呼び出す必要あり)

フィーチャースペックからシステムスペックへの移行

フィーチャースペックはレガシーな機能になりつつあるため、早めにシステムスペックに移行することが推奨される。フィーチャースペックをシステムスペックに移行する場合は次のような手順に従って移行する。

  1. Rails 5.1以上かつ、RSpec Rails 3.7以上になっていることを確認する
  2. システムスペックで使用する Capybara、Selenium Webdriver といった gem はなるべく最新のものを使うようにアップデートする
  3. js: true のタグが指定された場合にドライバが切り替わるように設定を変更する
  4. spec/features ディレクトリを spec/system にリネームする
  5. 各スペックのタイプを type: :feature から type: :system に変更する
  6. 各スペックで使われている feature を describe に変更する
  7. 各スペックで使われている background を before に変更する
  8. 各スペックで使われている given / given! を let / let! に変更する
  9. 各スペックで使われている scenario を it に変更する(この変更は任意)
  10. spec/rails_helper.rb の config.include などで、type: :feature になっている設定があれば type: :system に変更する

参考文献

この記事は以下の情報を参考にして執筆しました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?