チュートリアル「Rails Tutorial for Devise with RSpec and Cucumber」をやってみる

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に以下のライブラリを追加。

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"

RSpecのプロジェクトへのインストール

$ rails generate rspec:install

以下のファイルができる。

  • .rspec
  • spec/spec_helper.rb

テストのために 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>

単体テスト、統合テスト

RSpec単体テストを行う。全部成功する。

% rake spec

Cucumberで統合テストを行う。全部成功する。

% rake cucumber

おわりに

Deviseを使ったアプリケーションをつくるときには、このチュートリアルのspecファイル、featureファイルを参考に単体テストと統合テストを作ればよいはず。あとで、各ファイルをよく読んでみる。