Help us understand the problem. What is going on with this article?

初心者大歓迎!RSpec 3でドラッグアンドドロップ機能をテストする方法(スクリーンキャスト付き)

More than 5 years have passed since last update.

はじめに

前回投稿した記事では、ドラッグアンドドロップで表示順を変更できる簡単なサンプルアプリケーションを作成しました。

しかし、説明は実装が完了したところで終わっていて、テストは全く書いていませんでした。
せっかくなので、テストも書いておきましょう。

「ドラッグアンドドロップのテストってマウスなしできるの?」と思う方がいるかもしれませんが、最近のテストフレームワークはこうした操作もテストできるように作られています。
というわけで、本記事ではこのドラッグアンドドロップ機能をRSpec 3でテストする方法を説明します。

また、本記事ではRSpecを全く知らない人でもテストが書けるように、RSpecのセットアップ手順から説明しています。

前回の記事はこちらです

本記事では前回の記事で作成したサンプルアプリケーションにテストを追加していきます。
まだ読んでない人は先に読んでおくことをオススメします。

Rails 4で作るドラッグアンドドロップで表示順を変更できるサンプルアプリ(スクリーンキャスト付き)

補足資料

前回と同様、理解しやすくするための補足資料をいろいろ用意してみました。

デモサイト

サンプルアプリケーションのデモサイトを用意しています。
ドラッグアンドドロップするとどうなるか、実際に動かしてみてください。

http://sortable-table-sandbox.herokuapp.com/

sample-app.gif

スクリーンキャスト

本記事の内容を動画で紹介したスクリーンキャストです。
本記事では触れていない内容も説明しているので、初心者の方は見ておくと参考になる部分がいろいろとあるんじゃないかなーと思います。

https://www.youtube.com/watch?v=RGFz5P-NZn0&feature=youtu.be

Screen Shot 2014-08-02 at 7.17.20.png

トータルで38分ぐらいあるのでドラッグアンドドロップ機能のテストを追加するところから見たい人は、24分30秒あたりから再生してください。

https://www.youtube.com/watch?v=RGFz5P-NZn0&feature=youtu.be&t=24m30s

あと、画質も上げた方が字が潰れなくて見やすいはずです。

ソースコード

サンプルアプリケーションのソースコードはGitHubにアップしています。
こちらも参考にしてみてください。

https://github.com/JunichiIto/sortable-table-sandbox/tree/1st-rspec

参考文献

RSpecのセットアップ方法やフィーチャスペックの書き方等はこの電子書籍の内容を参考にしています。
スクリーンキャストの中でもときどき登場しています。

Everyday Rails - RSpecによるRailsテスト入門

テストを書くためのセットアップ

それでは早速テストを書いていきましょう!・・・と言いたいところですが、まず最初にRSpecのセットアップをしなくてはいけません。

以下の手順に従って、既存のRailsアプリケーションでRSpecのテストが書けるようにセットアップしてください。

PhantomJSのインストール

この記事ではPoltergeistというgemを使います。
このgemはPhantomJSに依存しているので、まずPhantomJSをインストールしておきます。

Homebrewを使っている人ならこんな感じでインストールします。

$ brew install phantomjs

詳しくはPoltergeist gemのREADMEか、ネット上のその他の情報を参考にしてみてください。

https://github.com/teampoltergeist/poltergeist#installing-phantomjs

RSpec関連のGemをインストール

RSpecで必要になる各種gemをインストールします。

Gemfile

 group :development do
   # (省略)
+  gem 'spring-commands-rspec'
 end
+group :development, :test do
+  gem 'rspec-rails', '~> 3.0.2'
+  gem 'factory_girl_rails', '~> 4.4.1'
+end
+group :test do
+  gem 'capybara', '~> 2.4.1'
+  gem 'poltergeist', '~> 1.5.1'
+  gem 'database_cleaner'
+end
$ bundle install

さらに、インストール用のコマンドを実行します。

$ rails g rspec:install

このコマンドを実行すると以下の3つのファイルが作成されます。

  • .rspec
  • spec/spec_helper.rb
  • spec/rails_helper.rb

warningsオプションを無効にする

.rspec はRSpecのデフォルトの実行オプションを指定するファイルです。
この中に入っている--warningsというオプションは困惑するぐらい大量の警告を吐くときがあるので、ここではこのオプションを無効にしておきます。

.rspec

  --color
- --warnings
  --require spec_helper

RSpecが実行できることを確認する

では試しにrspecコマンドを実行してみましょう。
まだテストは一つも書いていないので、"No examples found."と表示されますが、ここではエラーが発生しなければOKです。

$ bundle exec rspec
No examples found.

Finished in 0.03279 seconds (files took 2.3 seconds to load)
0 examples, 0 failures

Spring経由でRSpecを実行できるようにする

このサンプルアプリケーションはRails 4.1で作成しています。
Rails 4.1からはSpringというプリローダーが使えるようになっています。

RSpecもこのSpring経由で実行できるようにしておきましょう。
そうすればテストの起動時間をぐっと短くすることができます。

まず、spring-commands-rspec というgemを追加します。

Gemfile

   gem 'hub', :require=>nil
   gem 'quiet_assets'
   gem 'rails_layout'
+  gem 'spring-commands-rspec'
 end
 group :development, :test do
   gem 'rspec-rails', '~> 3.0.2'
$ bundle install

それから、次のコマンドを実行します。

$ bundle exec spring binstub rspec

このコマンドを実行すると、以下のファイルが作成されます。

  • bin/rspec

ターミナルからbin/rspecと入力すると、Spring経由でRSpecが実行されます。
おそらく、bundle exec rspecで実行したときよりもload時間が短くなっているはずです。

$ bin/rspec
No examples found.

Finished in 0.03218 seconds (files took 0.53729 seconds to load)
0 examples, 0 failures

FactoryGirlをセットアップする

本記事ではFactoryGirlというgemを使ってテストデータを作成します。
spec/factories/fruits.rb というファイルを作成し、以下のようなコードを書いてください。

spec/factories/fruits.rb

FactoryGirl.define do
  factory :fruit do
    name 'Apple'
  end
end

さらに、テストコード中にFactoryGirlのメソッドを短い記述で呼び出せるように、設定を変更します。

spec/rails_helper.rb

 ActiveRecord::Migration.maintain_test_schema!

 RSpec.configure do |config|
+  config.include FactoryGirl::Syntax::Methods
+
   # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
   config.fixture_path = "#{::Rails.root}/spec/fixtures"

この設定を入れると、FactoryGirl.build :fruit ではなく、 build :fruit というように、短い記述でFactoryGirlのメソッドが呼び出せるようになります。

testディレクトリを削除する

元からあったtestディレクトリはscaffoldが勝手に吐きだしたテストコードなので、ここでは使用しません。
不要なのでディレクトリごと削除してください。

$ rm -rf test

以上でRSpec関連のセットアップは完了です。

簡単なモデルスペックを書いてみる

ではお待ちかねのドラッグアンドドロップのテストに進みましょう!・・・と言いたいところですが、ちょっと準備運動をしておきましょう。

簡単なテストを書いてみて、ちゃんとRSpecのセットアップが完了していることを確かめます。
また、RSpec初心者の方にはここでRSpecの書き方に慣れてもらう、という目的もあります。

では、 spec/models/fruit_spec.rb というファイルを作成してください。
ここに以下のようなテストコードを書きます。

spec/models/fruit_spec.rb

require 'rails_helper'

describe Fruit do
  it 'returns name' do
    fruit = build :fruit, name: 'Banana'
    expect(fruit.name).to eq 'Banana'
  end
end

このテストは Fruit クラスに正しく name が設定されていることを検証するためのテストです。

fruit = build :fruit, name: 'Banana' の部分はFactoryGirlを使ってテストデータを作成しています。
意味としては fruit = Fruit.new(name: 'Banana') と同じだと思ってください。

ではテストを実行して、このテストがパスすることを確認してみましょう。

$ bin/rspec
.

Finished in 0.04652 seconds (files took 0.5476 seconds to load)
1 example, 0 failures

上のように "1 example, 0 failures" と表示されればOKです!

少し複雑なモデルスペックを書いてみる

続いて、もう少し複雑なモデルスペックを書いてみましょう。

「何もしなければデータが追加された順にFruitが並ぶこと」と、「順番の変更が正しく行われること」を検証します。
つまり、ranked-model gem が正しく機能しているかどうかを検証します。

先ほど作った fruit_spec.rb に以下のようなコードを追加してください。

spec/models/fruit_spec.rb

     fruit = build :fruit, name: 'Banana'
     expect(fruit.name).to eq 'Banana'
   end
+  describe 'ordering' do
+    let!(:apple) { create :fruit, name: 'Apple' }
+    let!(:banana) { create :fruit, name: 'Banana' }
+    let!(:orange) { create :fruit, name: 'Orange' }
+    it 'orders by registration' do
+      expect(Fruit.rank(:row_order)).to eq [apple, banana, orange]
+    end
+    it 'changes order' do
+      expect{
+        orange.update_attribute :row_order_position, :first
+      }.to change{
+        Fruit.rank(:row_order).to_a
+      }.from([apple, banana, orange]).to([orange, apple, banana])
+    end
+  end
 end

let!(:apple) { create :fruit, name: 'Apple' } はテスト実行前に { } の中の処理を実行し、戻り値を apple というローカル変数に設定します。(というようなものだと思ってください。)
イメージ的にはこんな感じです。

fruit = Fruit.create(name: 'Apple')

ここでは apple のほかに、 banana と orange も create しました。

「何もしなければデータが追加された順にFruitが並ぶこと」を検証する

「何もしなければデータが追加された順にFruitが並ぶこと」を検証しているのは以下の部分です。

it 'orders by registration' do
  expect(Fruit.rank(:row_order)).to eq [apple, banana, orange]
end

ここでは Fruit.rank(:row_order) の結果が期待値である [apple, banana, orange] に一致することを検証しています。

「順番の変更が正しく行われること」を検証する

一方、「順番の変更が正しく行われること」を検証しているのは以下の部分です。

注) 冒頭で紹介したスクリーンキャストではこのテストコードを説明するのを忘れていました。

it 'changes order' do
  expect{
    orange.update_attribute :row_order_position, :first
  }.to change{
    Fruit.rank(:row_order).to_a
  }.from([apple, banana, orange]).to([orange, apple, banana])
end

ちょっとややこしいですが、記法としては expect{ X }.to change{ Y }.from(A).to(B) になっています。
日本語にするなら「XするとYの結果がAからBに変わるはず」というような意味になります。

つまり、「orange.update_attribute :row_order_position, :first すると、 Fruit.rank(:row_order).to_a の結果が [apple, banana, orange] から [orange, apple, banana] に変わるはず」ということを検証しているわけです。

ややこしいのは一つ前のテストとは異なり、 Fruit.rank(:row_order) には .to_a を付けて強制的に配列に変換している点です。

これはRailsがデータベースへの問い合わせを評価される直前まで ActiveRecord::Relation として保持していることが原因なのですが、それを説明すると長くなるので今回はスキップします。

気になる方は以前僕が書いたこちらのQiita記事を読んでみてください。

RSpecでModelのテストをするときは、リレーションをchange from/toとしてテストしないように注意する

テストがパスすることを確認する

追加したテストの内容が理解できたら、テストを実行してみましょう。

$ bin/rspec
...

Finished in 0.10075 seconds (files took 0.54547 seconds to load)
3 examples, 0 failures

上のように、"3 examples, 0 failures" となればOKです。
次はフィーチャスペックに移ります。

単純なフィーチャスペックを書く

さて、いよいよドラッグアンドドロップのテストを書くときがやってきました!・・・と言いたいところですが、もう一回だけ準備運動をしておきましょう。

まず最初にごく簡単なフィーチャスペックを書いてフィーチャスペックに慣れてもらいます。

フィーチャスペックって何?

フィーチャスペックはいわゆる「統合テスト」です。
ユーザがブラウザを操作する様子をシミュレートし、その実行結果を検証できます。

rootページの初期表示を検証する

では、rootページの初期表示を検証するフィーチャスペックを書いてみましょう。

先にテストデータを作成し、それからrootページを表示して、データベースに登録されているFruitが全件表示されていることを検証します。

spec/features/fruits_spec.rb を作成して、次のようなコードを書いてください。

spec/features/fruits_spec.rb

require 'rails_helper'

feature 'Fruit pages' do
  given!(:apple) { create :fruit, name: 'Apple' }
  given!(:banana) { create :fruit, name: 'Banana' }
  given!(:orange) { create :fruit, name: 'Orange' }
  scenario 'Sort by drag-and-drop', js: true do
    visit root_path
    expect(page).to have_content 'Listing fruits'
    expect(page).to have_content 'Apple'
    expect(page).to have_content 'Banana'
    expect(page).to have_content 'Orange'
  end
end

フィーチャスペックではモデルスペックと異なるキーワード(メソッド)が登場します。
ですが、役割的にはほとんど変わりません。それぞれの対応関係は以下の通りです。

  • describe <=> feature
  • given! <=> let!
  • scenario <=> it

visit root_pathroot_path にアクセスすることを意味します。
expect(page).to have_content 'Listing fruits' は「現在のページに 'Listing fruits' が表示されていること」を検証しています。
同様に、事前にデータベースに登録しておいた 'Apple' と 'Banana' と 'Orange' が表示されていることも検証しています。

given! の部分は前述の let! と同じなので、テストの実行前に { } の中の処理を実行します。つまり、Fruitのデータを作成します。

テストコードの意味が理解できたら、テストを実行してパスすることを確認しましょう。

$ bin/rspec
....

Finished in 2.68 seconds (files took 0.49361 seconds to load)
4 examples, 0 failures

上のように、"4 examples, 0 failures" と表示されればOKです。

ドラッグアンドドロップのフィーチャスペックを書く

お待たせしました。いよいよここからが本編です。(今度こそ本当です!)
これからドラッグアンドドロップのテストを書いていきます。

ですが、その前にちょっとだけセットアップが必要です。

Poltergeist をセットアップする

ドラッグアンドドロップはJavaScriptを使って実現します。
JavaScriptの力を借りないと実現できない操作は、そのままフィーチャスペックとして書いても動きません。
なのでJavaScript用のwebドライバーをセットアップする必要があります。

本記事では Poltergeist をwebドライバーとして使うので、その設定を追加してください。
また、データベーストランザクションの扱いも変わってくるので、database_cleaner gemを使ってトランザクション関連の挙動も変更します。

spec/rails_helper.rb

 require 'spec_helper'
 require File.expand_path("../../config/environment", __FILE__)
 require 'rspec/rails'
+require 'capybara/poltergeist'

 # (省略)

 RSpec.configure do |config|
   config.include FactoryGirl::Syntax::Methods
+  Capybara.javascript_driver = :poltergeist

   # (省略)

-  config.use_transactional_fixtures = true
+  config.use_transactional_fixtures = false

+  require 'database_cleaner'
+  config.before(:suite) do
+    DatabaseCleaner.strategy = :truncation
+    DatabaseCleaner.clean_with(:truncation)
+  end
+
+  config.before(:each) do
+    DatabaseCleaner.start
+  end
+
+  config.after(:each) do
+    DatabaseCleaner.clean
+  end

   # (省略)

Viewに手を入れてテストしやすくする

ドラッグアンドドロップのテストを書くときは、ドラッグ/ドロップする対象のHTML要素をid指定できると便利です。
そこで、テーブルの tr タグにid属性を付けましょう。

ここでは "fruit-1" や "fruit-3" と行った具合に、"fruit-#{データベース上のid}" というid属性を付けていきます。

fruits/index.html.slim

 tbody
   - @fruits.each do |fruit|
-    tr.item(data = { model_name: fruit.class.name.underscore, update_url: fruit_sort_path(fruit) })
+    tr.item(data = { model_name: fruit.class.name.underscore, update_url: fruit_sort_path(fruit) } id = "fruit-#{fruit.id}")
       td = fruit.name
       td = link_to 'Show', fruit
       td = link_to 'Edit', edit_fruit_path(fruit)

ドラッグアンドドロップで順番が変わることを検証する

さーて、お待ちかねのドラッグアンドドロップのテストコードです!
といっても、Capybaraの力を借りれば拍子抜けするほど簡単に実現できちゃいます。

次のようなテストコードを書いてください。

spec/features/fruits_spec.rb

 require 'rails_helper'

 feature 'Fruit pages' do
   given!(:apple) { create :fruit, name: 'Apple' }
   given!(:banana) { create :fruit, name: 'Banana' }
   given!(:orange) { create :fruit, name: 'Orange' }
-  scenario 'Sort by drag-and-drop' do
+  scenario 'Sort by drag-and-drop', js: true do
     visit root_path
     expect(page).to have_content 'Listing fruits'
     expect(page).to have_content 'Apple'
     expect(page).to have_content 'Banana'
     expect(page).to have_content 'Orange'

+    source = page.find("#fruit-#{apple.id}")
+    target = page.find("#fruit-#{orange.id}")
+    expect{
+      source.drag_to(target)
+      sleep 0.2
+    }.to change{
+      Fruit.rank(:row_order).to_a
+    }.from([apple, banana, orange]).to([banana, orange, apple])
   end
 end

まず、 scenario に js: true というオプションを付けています。
このオプションは「このシナリオの実行する際はJavaScriptを使いますよ」という印付けで、webドライバとしてPoltergeistが使われるようになります。

そして、実際にドラッグアンドドロップするのが以下の部分です。

source = page.find("#fruit-#{apple.id}")
target = page.find("#fruit-#{orange.id}")
source.drag_to(target)

コードを見ると何となく意味がわかるんじゃないかと思います。
「source を target までドラッグする」と読めますね。

source には id="fruit-#{apple.id}" になっているHTML要素が入ります。
つまり、apple の行(trタグ)です。
データベース上のidは予想できないので、#{apple.id} という形で動的に変更しています。
target も同様に orange の行です。

よって、ここでは「apple を orange までドラッグする」ということになります。

検証用のコードは前述の「少し複雑なモデルスペック」にも登場した、expect{ X }.to change{ Y }.from(A).to(B) の構文です。
つまり、「apple を orange へドラッグすると、Fruit.rank(:row_order).to_a の結果が [apple, banana, orange] から [banana, orange, apple] に変わるはず」ということを検証しています。

注意すべきなのは sleep 0.2 の部分です。
実行環境によって異なるかもしれませんが、一瞬スリープさせないとJavaScriptの処理が完了する前に期待値の検証が始まってしまい、テストが失敗するケースがあります。
僕の環境でもテストが失敗してしまったので、意図的にスリープさせています。

さあ、テストを実行してみましょう。
ドラッグアンドドロップのテストコードはちゃんと動くでしょうか??

$ bin/rspec
....

Finished in 2.52 seconds (files took 0.51542 seconds to load)
4 examples, 0 failures

はい、このように全テストがパスすれば成功です!!

おまけ: テストが正しいことを検証するために、わざと失敗させてみる

注) このセクションはスクリーンキャストには含まれていません。

ここまで説明してきたとおりにテストを書けば、ドラッグアンドドロップのテストも一発でパスするはずです。

でも実はドラッグアンドドロップのテストが成功するのは幻だった・・・なんていう可能性はありませんか?
自分ではテストを書いて品質を担保したつもりだったのに、不完全なテストを書いてしまったがゆえに、ある日突然予期せぬ不具合が発生してしまった・・・なんてことが起きると最悪です。

そこで、わざとテストを失敗させてみましょう。
しかるべきタイミングで、ちゃんとテストが間違いを知らせてくれるかをチェックしてみます。
火災報知器の検査みたいなものですね。

というわけで、「apple から orange へ」ではなく、「apple から banana へ」ドラッグアンドドロップしてみます。

spec/features/fruits_spec.rb

 source = page.find("#fruit-#{apple.id}")
-target = page.find("#fruit-#{orange.id}")
+target = page.find("#fruit-#{banana.id}")

さて、テストを実行してみましょう。
失敗すれば成功です。(ややこしい)

$ bin/rspec
F...

Failures:

  1) Fruit pages Sort by drag-and-drop
     Failure/Error: expect{
       expected result to have changed to [#<Fruit id: 2, name: "Banana", created_at: "2014-08-01 13:10:09", updated_at: "2014-08-01 13:10:09", row_order: 4194304>, #<Fruit id: 3, name: "Orange", created_at: "2014-08-01 13:10:09", updated_at: "2014-08-01 13:10:09", row_order: 6291456>, #<Fruit id: 1, name: "Apple", created_at: "2014-08-01 13:10:09", updated_at: "2014-08-01 13:10:09", row_order: 0>], but is now [#<Fruit id: 2, name: "Banana", created_at: "2014-08-01 13:10:09", updated_at: "2014-08-01 13:10:09", row_order: 4194304>, #<Fruit id: 1, name: "Apple", created_at: "2014-08-01 13:10:09", updated_at: "2014-08-01 13:10:12", row_order: 5242880>, #<Fruit id: 3, name: "Orange", created_at: "2014-08-01 13:10:09", updated_at: "2014-08-01 13:10:09", row_order: 6291456>]
     # ./spec/features/fruits_spec.rb:16:in `block (2 levels) in <top (required)>'
     # -e:1:in `<main>'

Finished in 2.93 seconds (files took 0.56879 seconds to load)
4 examples, 1 failure

Failed examples:

rspec ./spec/features/fruits_spec.rb:7 # Fruit pages Sort by drag-and-drop

良かった、失敗しました!(なんか変)
ドラッグアンドドロップのテストはちゃんと意図した通りに機能しているようです。

もちろん、失敗することを確認したら、テストコードは元に戻しておきます。

spec/features/fruits_spec.rb

 source = page.find("#fruit-#{apple.id}")
-target = page.find("#fruit-#{banana.id}")
+target = page.find("#fruit-#{orange.id}")

まとめ

いかがだったでしょうか?
「ドラッグアンドドロップ機能のテストを自動化する」っていうと、一瞬「ムリ!」と思ってしまいそうですが、便利なgemのおかげで意外と簡単に実現できたりします。

記事の本文の中では簡略化して説明している部分も多いので、初心者の方はスクリーンキャストも一緒に見てもらった方がわかりやすいと思います。

また、スクリーンキャストの中でもときどき登場していますが、「Everyday Rails - RSpecによるRailsテスト入門」は初心者向けの良い学習資料になるのはもちろん、中級者以上の方にとっても書き方を忘れたときのリファレンスとして便利な一冊です。
PCに入れておけば、本棚まで歩かなくてもさっと情報を取り出せます。

まだ読んだことがない方は一度チェックしてみてください。

Everyday Rails - RSpecによるRailsテスト入門

あわせて読みたい

RSpecの入門記事が読みたい方へ

RSpec 3の新機能が気になる方へ

RSpec 2からRSpec 3へアップグレードしたい方へ

jnchito
SIer、社内SEを経て、ソニックガーデンに合流したプログラマ。 「プロを目指す人のためのRuby入門」の著者。 http://gihyo.jp/book/2017/978-4-7741-9397-7 および「Everyday Rails - RSpecによるRailsテスト入門」の翻訳者。 https://leanpub.com/everydayrailsrspec-jp
https://blog.jnito.com/
sonicgarden
「お客様に無駄遣いをさせない受託開発」と「習慣を変えるソフトウェアのサービス」に取り組んでいるソフトウェア企業
http://www.sonicgarden.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした