はじめに
前回投稿した記事では、ドラッグアンドドロップで表示順を変更できる簡単なサンプルアプリケーションを作成しました。
しかし、説明は実装が完了したところで終わっていて、テストは全く書いていませんでした。
せっかくなので、テストも書いておきましょう。
「ドラッグアンドドロップのテストってマウスなしできるの?」と思う方がいるかもしれませんが、最近のテストフレームワークはこうした操作もテストできるように作られています。
というわけで、本記事ではこのドラッグアンドドロップ機能をRSpec 3でテストする方法を説明します。
また、本記事ではRSpecを全く知らない人でもテストが書けるように、RSpecのセットアップ手順から説明しています。
前回の記事はこちらです
本記事では前回の記事で作成したサンプルアプリケーションにテストを追加していきます。
まだ読んでない人は先に読んでおくことをオススメします。
Rails 4で作るドラッグアンドドロップで表示順を変更できるサンプルアプリ(スクリーンキャスト付き)
補足資料
前回と同様、理解しやすくするための補足資料をいろいろ用意してみました。
デモサイト
サンプルアプリケーションのデモサイトを用意しています。
ドラッグアンドドロップするとどうなるか、実際に動かしてみてください。
スクリーンキャスト
本記事の内容を動画で紹介したスクリーンキャストです。
本記事では触れていない内容も説明しているので、初心者の方は見ておくと参考になる部分がいろいろとあるんじゃないかなーと思います。
トータルで38分ぐらいあるのでドラッグアンドドロップ機能のテストを追加するところから見たい人は、24分30秒あたりから再生してください。
あと、画質も上げた方が字が潰れなくて見やすいはずです。
ソースコード
サンプルアプリケーションのソースコードはGitHubにアップしています。
こちらも参考にしてみてください。
参考文献
RSpecのセットアップ方法やフィーチャスペックの書き方等はこの電子書籍の内容を参考にしています。
スクリーンキャストの中でもときどき登場しています。
Everyday Rails - RSpecによるRailsテスト入門
テストを書くためのセットアップ
それでは早速テストを書いていきましょう!・・・と言いたいところですが、まず最初にRSpecのセットアップをしなくてはいけません。
以下の手順に従って、既存のRailsアプリケーションでRSpecのテストが書けるようにセットアップしてください。
PhantomJSのインストール
この記事ではPoltergeistというgemを使います。
このgemはPhantomJSに依存しているので、まずPhantomJSをインストールしておきます。
Homebrewを使っている人ならこんな感じでインストールします。
$ brew install phantomjs
詳しくはPoltergeist gemのREADMEか、ネット上のその他の情報を参考にしてみてください。
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_path
は root_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の入門とその一歩先へ ~RSpec 3バージョン~
- RSpecの入門とその一歩先へ、第2イテレーション ~RSpec 3バージョン~
- RSpecの入門とその一歩先へ、第3イテレーション ~RSpec 3バージョン~
RSpec 3の新機能が気になる方へ
RSpec 2からRSpec 3へアップグレードしたい方へ