The RSpec Bookをやってみるで、RSpec + Cucumberが良さそうなことはわかったのだけど、具体的にどう適用したらよいのかわからない。特にほとんどのアプリで最初に作るべきログイン機能の部分。
そこで、Rails Tutorial for Devise with RSpec and Cucumberをやってみる。
新しいプロジェクト作成
% rails new rails3-devise-rspec-cucumber -T
「-T」は、Unit::Testを入れないためのオプション。理由はRSpecを入れるため。
Gemfileに以下のライブラリを追加。
- rspec-rails
- cucumber-rails
- database_cleaner
- factory_girl_rails (参考:func09:has_manyなフィクスチャを書くのに疲れたらFactory Girlがオススメ!、テストフィクスチャーの準備用)
- email_spec (参考:UKSTUDIO:Cucumber+email_specでActionMailerのテストをする、e-mail機能のテスト)
- capybara (おもしろWEBサービス開発日記:Capybara の README 意訳、Webアプリテスト用)
- devise
- execjs
- therubyracer
source 'https://rubygems.org' gem 'rails', '3.2.8' gem 'sqlite3' group :assets do gem 'sass-rails', '~> 3.2.3' gem 'coffee-rails', '~> 3.2.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'execjs' gem 'therubyracer' group :development, :test do gem "rspec-rails", ">= 2.10.1" gem "factory_girl_rails", ">= 3.3.0" end group :test do gem "email_spec", ">= 1.2.1" gem "cucumber-rails", ">= 1.3.0", :require => false gem "capybara", ">= 1.1.2" gem "database_cleaner", ">= 0.7.2" gem "launchy", ">= 2.1.0" end gem "devise", ">= 2.1.0"
テストのために Factory Girl用のspecファイルを作成する。
% cd spec % mkdir factories % touch users.rb
spec/factories/users.rbの中見は以下の通り
FactoryGirl.define do factory :user do name 'Test User' email 'example@example.com' password 'please' password_confirmation 'please' # required if the Devise Confirmable module is used # confirmed_at Time.now end end
もし、deviseのConfirmableモジュールを使う場合は、「confirmed_at Time.now」の行をコメントインする。
Devise Test Helpersを追加する
Deviseにおいて、ログイン後にのみ操作を行わせたい場合は「before_filter :authenticate_user!」がよく使われる。Your tests will fail unless a default user is created and logs in before each test runs. Devise provides test helpers to make it simple to create and log in a default user.
% mkdir spec/support % touch spec/support/devise.rb
spec/support/devise.rb の中身は以下のようにする。
RSpec.configure do |config| config.include Devise::TestHelpers, :type => :controller end
Now you can write controller specs that set up a signed-in user before tests are run.
Email Spec
Eメールに関するテストができるように Email Specを読み込む。
% cp -p spec/spec_helper.rb spec/spec_helper.rb.org % vi spec/spec_helper.rb
編集内容は以下のとおり
# in spec/support/ and its subdirectories. Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} RSpec.configure do |config| config.include(EmailSpec::Helpers) # この行を加える。 config.include(EmailSpec::Matchers) # この行を加える。 # ## Mock Framework
差分で示すと以下のとおり
% diff spec_helper.rb.org spec_helper.rb 11a12,14 > config.include(EmailSpec::Helpers) > config.include(EmailSpec::Matchers) >
HelperとViewerの単体テスト(Unit test)を飛ばす
Cucamberを使うので、Railsが自動で生成するHelperとViewerの単体テストは不要とのこと。config/application.rbに以下を加える。
# don't generate RSpec tests for views and helpers config.generators do |g| g.view_specs false g.helper_specs false end
RSpecを動かしてみる。
まずは、rake一覧にRSpecのタスクがでるかを確認する。
% rake -T | grep spec
データベースの準備をする。
% bundle exec rake db:migrate % bundle exec rake db:test:prepare
RSpecを動かしてみる。以下のようになったら正常に稼働している。
% rake spec No examples matching ./spec{,/*/**}/*_spec.rb could be found
チュートリアルで既に準備しているspecファイル(テストのためのチェックファイル)をダウンロードする。
% cd spec % mkdir controllers % cd controllers % curl -o home_controller_spec.rb https://raw.github.com/RailsApps/rails3-devise-rspec-cucumber/master/spec/controllers/home_controller_spec.rb % curl -o users_controller_spec.rb https://raw.github.com/RailsApps/rails3-devise-rspec-cucumber/master/spec/controllers/users_controller_spec.rb % cd ../ % mkdir models % cd models % curl -o user_spec.rb https://raw.github.com/RailsApps/rails3-devise-rspec-cucumber/master/spec/models/user_spec.rb % cd ../../ % rake spec ... uninitialized constant UsersController (NameError) ...
これは正常稼働。このメッセージが出る理由は、UsersControllerなどを作成していないから。
Cucumberの導入
Cucamberをこのプロジェクトに導入する。
% % rails g cucumber:install --capybara --rspec
「--capybara」オプションはWebratではなくCapybaraを使うという指定。「--rspec」オプションはRSpecと連動させるという指定。
CucumberとEmail Specとの連携
% touch features/support/email_spec.rb
features/support/email_spec.rbの中身は以下のとおり。
require 'email_spec/cucumber'
Cucumberのシナリオに対応する動作を書くstepファイルを生成する。
% rails generate email_spec:steps
Featuresを実行するときの入力を省略する。
cucumberを実行するときは例えば以下のように入力する。
% bundle exec cucumber features/visitors/request_invitation.feature --require features
「--require features」を省略するためには、config/cucumber.ymlに以下を加える。
std_opts = "-r features/support/ -r features/step_definitions --format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip"
Cucumberを実行する
% rake cucumber Using the default profile... 0 scenarios 0 steps 0m0.000s
何もシナリオがないので上記で正常稼働。
チュートリアル用のシナリオやStepファイルを取得する。
% cd features % cd support % curl -o paths.rb https://raw.github.com/RailsApps/rails3-devise-rspec-cucumber/master/features/support/paths.rb % cd ../ % cd step_definitions % curl -o user_steps.rb https://raw.github.com/RailsApps/rails3-devise-rspec-cucumber/master/features/step_definitions/user_steps.rb % cd ../ % mkdir users % cd users % curl -o sign_in.feature https://raw.github.com/RailsApps/rails3-devise-rspec-cucumber/master/features/users/sign_in.feature % curl -o sign_out.feature https://raw.github.com/RailsApps/rails3-devise-rspec-cucumber/master/features/users/sign_out.feature % curl -o sign_up.feature https://raw.github.com/RailsApps/rails3-devise-rspec-cucumber/master/features/users/sign_up.feature % curl -o user_edit.feature https://raw.github.com/RailsApps/rails3-devise-rspec-cucumber/master/features/users/user_edit.feature % curl -o user_show.feature https://raw.github.com/RailsApps/rails3-devise-rspec-cucumber/master/features/users/user_show.feature % cd ../../
Devise Confirmable moduleを使っている場合についての指示は省略。
Cucumberで統合テストをしてみる。ただし失敗する。
% rake cucumber
TDD、BDDはこのテスト失敗をつぶしていくことでソフトウェア開発をする。
Emailの設定
開発モード(develpmentモード)でメールを送らない設定にしてあるのを変更する。config/environments/development.rbの下記部分をコメントアウトする
# Don't care if the mailer can't send config.action_mailer.raise_delivery_errors = false #コメントアウトする
そして、以下を付け加える。
# ActionMailer Config config.action_mailer.default_url_options = { :host => 'localhost:3000' } config.action_mailer.delivery_method = :smtp # change to false to prevent email from being sent during development config.action_mailer.perform_deliveries = true #config.action_mailer.raise_delivery_errors = false config.action_mailer.raise_delivery_errors = true config.action_mailer.default :charset => "utf-8"
config/environments/production.rbに以下を付け加える。
config.action_mailer.default_url_options = { :host => 'example.com' } # ActionMailer Config # Setup for production - deliveries, no errors raised config.action_mailer.delivery_method = :smtp config.action_mailer.perform_deliveries = true config.action_mailer.raise_delivery_errors = false config.action_mailer.default :charset => "utf-8"
config/environments/test.rbに以下を付け加える。
# ActionMailer Config config.action_mailer.default_url_options = { :host => 'example.com' }
本番環境の'example.com'は適切なホスト名に変える。テスト環境はどんなホスト名を指摘しても良い。
Deviseの設定
このプロジェクトにdeviseを導入する。
% rails g devise:install
EmailのためのDeviseの設定
config/initializers/devise.rb を編集する。
- config.mailer_sender でFrom欄のメールアドレスを設定
Userモデルの作成
% rails generate devise User
spec/models/user_spec.rb と spec/factories/users.rb も自動作成してくれるが、先にチュートリアル用のファイルをダウンロード済みなので、上書き保存しないように注意する。
config/routes.rb に以下が追加されている。
devise_for :users
CucumberとDeviseを使うときの注意事項
Devise 1.4.2以降、Sign out時にはHTTPのDELETEメソッド(RailsではJavascriptを使って実現)を用いるようになったが、Cucumberを用いたテストでは、GETメソッドを用いることを期待している。なので、testのときだけ、GETメソッドを使うように設定を変更する必要がある。
/config/initializers/devise.rbの下記部分がその変更設定。
# The default HTTP method used to sign out a resource. Default is :delete. config.sign_out_via = Rails.env.test? ? :get : :delete
ログにパスワードを表示しないようにする
config/application.rbに以下を追加する
config.filter_parameters += [:password, :password_confirmation]
ユーザー名をUserモデルに付け加える
deviseでは、メールアドレスをユーザ識別名として使う。ユーザー名を別途追加したい場合は、以下の手順で追加する。
% rails generate migration AddNameToUsers name:string
nameとemailがユニークな値でかつ空であることがないようにする場合は、app/models/user.rb でチェックをかける。
validates_presence_of :name validates_uniqueness_of :name, :email, :case_sensitive => false
mass-assignmentによる値の書き換えを防ぐために、以下を追記する。
attr_accessible :name, :email, :password, :password_confirmation, :remember_me # :nameを追記
余談
The RSpec Bookだと、cucumberやRSpecでテスト→失敗しているとこ確認→そこを作成→cucumberやRSpecでテスト→…と続いていくが、このチュートリアルは作成部分をまとめて行っているみたい。
適宜 rake cucumber を実行して、いくつのシナリオがパスしているのかをみてみると良い。
ユーザ登録Viewのカスタマイズ
deviseはデフォルトでは、コントローラーおよびビューを編集不要なので隠しているが、カスタマイズしたいときがある(たとえば、nameという新たな属性を追加した場合とか)。そのときは以下のように進める。
ビューをコピーする。
% rails g devise:views
app/views/devise/registrations/edit.html.erb とapp/views/devise/registrations/new.html.erb に名前入力欄を加える。
<div><%= f.label :name %><br /> <%= f.text_field :name %></p>
デフォルトトップページの削除
% rm public/index.html
Homeコントローラーの作成
アプリケーションの最初のページとしてHomeコントローラーを用意する。
% rails generate controller home index --no-controller-specs
すでにhomeコントローラーのspecファイルはダウンロード済みなので、「--no-controller-specs」オプションでspecファイルの生成を止める。
config/routes.rb でトップページをhome/indexにする。
authenticated :user do root :to => 'home#index' # 認証に成功したときのトップページ end root :to => "home#index" # すべての閲覧者に対するトップページ
上記のように設定するとログイン後のトップページと任意の閲覧者に対するトップページを簡単に変更することができる。deviseのデフォルトでは、ログイン成功後はroot_pathに飛ぶ。
確認してみる。
% rails server
http://localhost:3000/ にアクセスし、 home#indexが表示されればちゃんとできている。
home#indexの編集
チュートリアル用にユーザー一覧を出してみる。
app/controllers/home_controller.rbに以下を加える。
def index @users = User.all end
app/views/home/index.html.erb に以下を加える。
<h3>Home</h3> <% @users.each do |user| %> <p>User: <%= user.name %> </p> <% end %>
テスト用にデフォルトユーザーを作る
db/seeds.rb に以下を加える。
puts 'SETTING UP DEFAULT USER LOGIN' user = User.create! :name => 'First User', :email => 'user@example.com', :password => 'please', :password_confirmation => 'please' puts 'New user created: ' << user.name user2 = User.create! :name => 'Second User', :email => 'user2@example.com', :password => 'please', :password_confirmation => 'please' puts 'New user created: ' << user2.name
上の設定を読み込ませる。
% bundle exec rake db:seed
Userのトップページを作る
indexとshowメソッドを持ったUserコントローラーを作成する。
% rails generate controller users index show --no-controller-specs
app/controllers/users_controller.rbを以下のように編集する。
class UsersController < ApplicationController before_filter :authenticate_user! def index @users = User.all end def show @user = User.find(params[:id]) end end
config/routes.rbに以下が加えられているはず。
get "users/index" get "users/show"
上記を削除して以下のように直す。
authenticated :user do root :to => 'home#index' end root :to => "home#index" devise_for :users resources :users, :only => [:show, :index]
なお、 devise_for :users は、resources :users, :only => [:show, :index] の上になければならない。
app/views/users/show.html.erb を以下のようにする。
<p>User: <%= @user.name %></p> <p>Email: <%= @user.email if @user.email %></p>
app/views/users/index.html.erb を以下のようにする。
<ul class="users"> <% @users.each do |user| %> <li> <%= link_to user.name, user %> signed up <%= user.created_at.to_date %> </li> <% end %> </ul>
app/views/home/index.html.erb を以下のように変更する。
<h3>Home</h3> <% @users.each do |user| %> <p>User: <%=link_to user.name, user %></p> <% end %>
レイアウトファイルの作成
Rails Application Layout for HTML5にしたがってレイアウトファイルを作成する。
app/views/layouts/_navigation.html.erb を作成し、中身を以下のようにする。
<%= link_to "Home", root_path, :class => 'brand' %> <ul class="nav"> <% if user_signed_in? %> <li> <%= link_to 'Logout', destroy_user_session_path, :method=>'delete' %> </li> <% else %> <li> <%= link_to 'Login', new_user_session_path %> </li> <% end %> <% if user_signed_in? %> <li> <%= link_to 'Edit account', edit_user_registration_path %> </li> <% else %> <li> <%= link_to 'Sign up', new_user_registration_path %> </li> <% end %> </ul>
app/views/layouts/_messages.html.erbを作成し、以下のようにする。
<% flash.each do |name, msg| %> <%= content_tag :div, msg, :id => "flash_#{name}" if msg.is_a?(String) %> <% end %>
app/views/layouts/application.html.erbを以下のように変える。
<!doctype html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>App_Name</title> <meta name="description" content=""> <meta name="author" content=""> <%= stylesheet_link_tag "application", :media => "all" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tags %> </head> <body> <div id="container" class="container"> <header> <%= render 'layouts/navigation' %> <%= render 'layouts/messages' %> </header> <div id="main" role="main"> <%= yield %> </div> <footer> </footer> </div> <!--! end of #container --> </body> </html>