2011年9月8日木曜日

Rails3を初歩から学ぶ #17 インテグレーションテスト

この一連のエントリは
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。

未認証状態で「http://localhost:3000/users/」でアクセスするとフィルタで弾かれてサインイン画面へ遷移します。
そしてサインインするとユーザーごとの個人ページ(/users/:id)に遷移します。
これはちょっと不親切ですね。
最初にindexを表示しようとしていたのだから、そこへ行ってあげるのが親切でしょう。
じゃあそうしましょうか、というとき、これはどうやって検証すればよいのでしょうか。
ここで検証したいのは「未認証状態でindex -> サインインページへ遷移 -> サインインしてindexへ」という一連の流れを検証したいのですが今までのRSpecファイルでは対応できそうもありません。

そこで登場するのがインテグレーションテストです。
まずはrailsコマンドでインテグレーションテストを生成します。
rails generate integration_test user_session
generateコマンドにintegration_testを指定するとインテグレーションテストを生成します。ここではuser_sessionという名前で生成しています。
実際に生成されるのは spec/requests/user_session_spec.rb です。早速編集しましょう。

# -*- coding: utf-8 -*-
require 'spec_helper'
describe "サインイン後の遷移先" do
it "サインイン前に要求したページに遷移する" do
user = Factory(:user)
visit users_path
#この段階でサインインページにリダイレクトされているハズ
fill_in :name, :with => user.name
fill_in :password, :with => user.password
click_button
#サインイン前に要求していたindexに遷移すること
response.should render_template('users/index')
end
end
だいたい英文のようになっているので何しているか分かると思います。
それでは実装に入りましょう。まずはアクセス拒否時にアクセス先を記録する処理と、サインイン後にリダイレクト先を決定するヘルパメソッドを追加します。
app/helpers/session_helper.rb に以下のメソッドを追加します。

def deny_access
store_location
redirect_to new_session_path, :notice => "サインインしてください"
end
def redirect_back_or(default)
redirect_to(session[:return_to] || default)
clear_return_to
end
※以下2メソッドはprivateで定義する
def store_location
session[:return_to] = request.fullpath
end
def clear_return_to
session[:return_to] = nil
end

deny_accessメソッドは既に存在していますが赤文字部分を追加します。
このstore_locationメソッドでセッションに要求先URLを保存します。
そしてredirect_back_orメソッドでセッションをチェックして直前にアクセス拒否されていたら拒否されたページにリダイレクトしています。
リダイレクトしたらセッションからアクセス先情報を削除しています。
あとはサインイン成功時のリダイレクト処理をredirect_back_orに置き換えればOKです。
app/controller/sessions_controller.rbのcreateメソッドを以下の赤文字部分のように修正します。

def create
user = User.authenticate(params[:session][:name],
params[:session][:password])
if user.nil?
flash.now[:error] = "サインインできませんでした"
@title = "サインイン"
render 'new'
else
sign_in user
redirect_back_or user
end
end
これまでredirect_toとしていた箇所を先ほど追加したredirect_back_orに置き換えています。これでインテグレーションテストもパスします。
実際に動かして試してみましょう。

2011年9月7日水曜日

ActionMailerをGmail経由で使いたい

RailsのActionMailerでgmailを使いたいのだ。
やり方はこちら。


若干、記事が古いのが気になる。
当たって砕けましょう。

まずRails3だとプラグインをインストールするコマンドが違います。
rails plugin install git://github.com/adamwiggins/gmail_smtp.git
あとは書いてある通りなのだけど、Ruby1.8.7以降を使っている場合は上記記事のコメント欄に書いてある修正が必要になります。
「vendor/plugins/gmail_smtp/lib/smtp_tls.rb」に対してコメント欄の通りの修正を加えます。
check_auth_args user, secret, authtype if user or secret という箇所が最初の方にあるので、、、

if RUBY_VERSION > "1.8.6"
 check_auth_args user, secret
else
 check_auth_args user, secret, authtype if user or secret
end

とします。
これをHerokuにアップデートすればgmail経由で送信できるようになります。
config/environment.rbは手を入れずにOK.


fakerがあるとHerokuでpushできない

なぜかfakerを使っているとHerokuでpushした際に
no such file to load -- faker
などと言われて失敗します。
対処法はこちら

Gemfileでfakerを指定している箇所を「:require => false」としたうえで、sample_data.rakeの「require 'faker'」を削除してやります。
まあfakerは使えなくなりますが、とりあえずpushできるようにはなります。

2011年9月6日火曜日

Rails3を初歩から学ぶ#16 サインアウト

この一連のエントリは
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。

前回まででサインインに関する処理が一通り完成したので、今度はサインアウトを実装しましょう。
spec/controller/sessions_controller_spec.rb に以下の検証コードを追加します。

describe "DELETE 'destroy'" do
it "未認証状態になり入り口に戻る" do
user = Factory(:user)
controller.sign_in(user)

delete :destroy
controller.should_not be_signed_in
response.should redirect_to(root_path)
end
end #DELETE 'destroy'
もう目新しいことはありません。
次にsign_inに対応するsign_outをヘルパメソッドとして定義しましょう。
app/helpers/sessions_helper.rbに追加します。

def sign_out
cookies.delete(:remember_token)
end

これだけです。次にdeleteアクションを実装しましょう。
sessions_controller.rbを編集します。
ちなみにusers_controller.rbじゃないです。意味的にこちらのdeleteはユーザーを削除することになるので、実装はいつかそのうちに。

def destroy
sign_out
redirect_to root_path
end
destroyアクションは以前sessions_controller.rbを生成した際に既に空で定義されているハズです。なのでそこに先ほど定義したサインアウトメソッドとリダイレクトの呼び出しを追加するのみです。
アプリケーションの入り口を示す意味で「root_path」を指定していますが、これはまだ未定義なのでconfig/route.rbに定義します。

resources :users
resources :sessions, :only => [:new, :create, :destroy]
match '/sessions/delete', :to => 'sessions#destroy' root :to => 'sessions#new'
赤字の箇所が追加分です。
ここではルートとしてサインインページを指定しています。
この状態で「http://localhost:3000/」とするとサインインページに行って欲しいんですが、public/index.htmlというファイルが存在すると、これが強制的に表示されます。
なのでrmコマンドで削除しておきましょう。
後はどこかにサインアウト用のリンクを追加すれば完成です。
これを期に各ページ共通のヘッダ部分を定義しましょう。
部分テンプレートとして定義します。
app/views/layouts/_header.html.erb を作成して以下のように編集します。
<header>
<nav class="round">
<ul>
<% if signed_in? %>
<li><%= link_to "ユーザー一覧", users_path %></li>
<li><%= link_to "プロフィール", current_user %></li>
<li><%= link_to "設定変更", edit_user_path(current_user) %></li>
<li><%= link_to "サインアウト", sessions_delete_path, :method => :delete %></li>
<% else %>
<li><%= link_to "サインイン", new_session_path %></li>
<% end %>
</ul>
</nav>
</header>
まとめてリンクを設定しました。設定変更のedit_user_pathだけは現時点で該当するアクションを実装していませんが、ついでに定義してしまいました。
こちらは次回作成しましょう。
それでは app/views/layouts/application.html.erb に今作成した部分テンプレートの呼び出しを追加します。

<div class="container">
<%= render 'layouts/header' %>
<section class="round">
<% flash.each do |key, value| %>
<%= content_tag(:div, value, :class => "flash #{key}") %>
<% end %>
</section>
<%= yield %>
</div>
renderメソッドでlayouts/headerを指定しています。これで部分テンプレート「_header.html.erb」が呼ばれます。その他、見た目用に少し手を入れています。CSS未設定なので見た目もへったくれも無いですが。


ここまで出来たらテストを実行してNGが0件であることを確認して、ブラウザで動作を見てみましょう。

ではテストを実行して全てパスすることを確認したら実際に動かして確認してみましょう。

2011年9月5日月曜日

BambooとCedar

Herokuを使ってみようと思ってドキュメントを読んでいたのだけど、Cedarって何よ。
Bambooって何ぞ。
ちんぷんかんぷんじゃないか。
というわけで調べてみました。

CedarというのはBambooの後継でRails3以降を使うならCedarを使わないといけない。
で、Bambooって何よというところで参考は以下。


Heroku上でRailsを動かすプラットフォームという位置づけ。
つまり、、、それは何ですか???
WEBrickみたいのもの?
オフィシャルを当たりましょう。英語だけど。


ここでもHerokuのランタイムだよ、と書いてある。
Java VMか、Dalvikか、という感じと解釈すればいいのかな。



2011年9月4日日曜日

Rails3を初歩から学ぶ #15 部分テンプレート

この一連のエントリは
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。

前回までで、ページごとの認証チェックとサインイン処理が完成しました。
今度は登録ユーザーの一覧が見えるindexアクションを実装しましょう。
例によってテストから作成するのですが、ユーザー一覧を検証したいのでユーザー数はある程度欲しいところではあります。
以前導入したFactoryGirlの機能を使えばそれも簡単です。
spec/factories.rb に以下の定義を追加します。

Factory.sequence :name do |n|
"Tarou#{n}"
end
このようにFactory.sequenceメソッドを使って定義すると「Factory.next(:name)」とすることで続々と「Tarou1」「Tarou2」とユーザー名を生成することが出来ます。
では試験コードです。
spec/controller/users_controller_spec.rbに以下のindexアクション用の検証コードを追加します。

describe "GET 'index'" do
describe "未認証ユーザーの検証"do
it "サインインページに飛ばす" do
get :index
response.should redirect_to(new_session_path)
flash[:notice].should =~ /サインインしてください/
end
end #未認証ユーザーの検証
describe"認証済みユーザーの検証" do
before(:each) do
@user = Factory(:user)
controller.sign_in(@user)
second = Factory(:user, :name => "Jirou")
third = Factory(:user, :name => "Saburou")
@users = [@user, second,third]
30.times do
@users << Factory(:user, :name => Factory.next(:name))
end
end
it "アクセスできること" do
get :index
response.should be_success
end
it "タイトル検証" do
get :index
response.should have_selector("title", :content => "ユーザー一覧")
end
it "ユーザー名のリストを表示していること" do
get :index
@users[0..2].each do |user|
response.should have_selector("li", :content => user.name)
end
end
end #認証済みユーザーの検証
end #GET index


特に目新しいものはないんですが、認証済みユーザーの検証をするbeforeブロックで先ほど定義したFactory.next(:user)を使ってユーザー名を自動生成しています。
ではアクションを定義しましょう。

before_filter :authenticate, :only => [:show, :index]
def index
@title = "ユーザー一覧"
@users = User.all
end
前回定義したbefore_filterにindexアクションを追加します。
複数指定するときは配列になります。
indexアクションではUser.allで全ユーザーを取得しています。
次はindexに対応したviewを作成します。app/views/users/index.html.erbファイルを作成した以下のように編集します。

<h1>ユーザー一覧</h1>
<ul class="users">
<%= render @users %>
</ul>
複数存在するユーザーごとに同じタグを適用することになるので、ここでは部分テンプレートを使用しています。renderメソッドに渡した値に応じた部分テンプレートに該当部分が置き換えられます。
通常は「render 'hoge'」などと指定して、対応する「_hoge.html.erb」に置き換えられます。ここではindexアクションで格納したUser配列をまとめて渡しています。こうすると自動的に1つ1つの要素に分解して部分テンプレートを適用してくれます。
このとき対応するテンプレートは「_user.html.erb」となります。部分テンプレートの先頭には「_」がつきます。
ではその_user.html.erbを以下のように作成しましょう。

<li>
<%= link_to user.name, user %>
</li>
link_toメソッドは1つめのパラメータを値に、2つ目のパラメータをリンク先にaタグを生成します。上記の例ではリンク先としてuserを渡していますが、これも例によって「/users/:id」に変換されます。

2011年9月2日金曜日

Rails3を初歩から学ぶ #14 アクセス制限してみよう

この一連のエントリは
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。

前回まででサインイン処理を実現しました。
が、認証せずに「http://localhost:3000/users/1」とするとアクセスできてしまいます。
認証済みユーザー以外はアクセスして欲しくないページには認証を事前に確認するようにしましょう。
spec/controller/users_controller_spec.rbを編集します。

describe "GET 'show'" do
describe "サインインしていなければアクセス禁止" do
before(:each) do
@user = Factory(:user)
end
it "サインインページにリダイレクトされること" do
get :show, :id => @user
response.should redirect_to(signin_path)
end
end #サインインしていなければアクセス禁止

describe "サインインしていればアクセス可能" do
before(:each) do
@user = Factory(:user)
controller.sign_in(@user)
end
it "should be successful" do
get :show, :id => @user
response.should be_success
end
it "正しいユーザーを表示していること" do
get :show, :id => @user
assigns(:user).should == @user
end
it "タイトルの検証" do
get :show, :id => @user
response.should have_selector("title", :content => @user.name)
end
end #サインインしていればアクセス可能
end #GET 'show'
赤字の部分を追加します。
既存の検証コードはbeforeブロックでサインインするようにしています。
そして今回新たにサインインしていないときはサインイン画面に遷移することを検証しています。では実装しましょう。
まずは認証済みかどうかをチェックして、認証していない場合はサインインページに飛ばす処理ですが、これは色々と使い回しそうなのでヘルパメソッドとして定義しておきます。
app/helpers/sessions_helper.rb に以下の2つのメソッド(public)を追加します。

def authenticate
deny_access unless signed_in?
end
def deny_access
redirect_to new_session_path, :notice => "サインインしてください"
end
signed_in?メソッドは前回実装したものでcookieの情報から認証状態をチェックします。
これでnilが返った場合はdeny_accessメソッドによりサインインページに飛ばしています。
ではこのヘルパを呼び出しましょう。
app/controller/users_controller.rbに以下の行を追加します。
before_filter :authenticate, :only => :show
これでshowアクション実行前に先ほど定義したauthenticateが呼ばれるようになります。
これでテストもパスするようになるので実際にサーバからサインインせずに
「http://localhost:3000/users/1」などとアクセスしてみてください。
サインインページに飛ばされます。

で、この状態で新規登録ページからユーザーを登録してみると、サインページに飛んでしまいます。実はまだユーザー登録時にサインインを処理していないので未認証状態なんですね。それはまあ良しとして問題はテストを実施してもNG0件なってしまうことです。
users_controller_spec.rbにてサインイン時に以下のような検査コードがあります。
response.should redirect_to(user_path(assigns(:user)))
これはサーバからusers/:idへのリダイレクトメッセージが返ることを検証しているもので、確かにこの検証からすればNGにはなりません。今、問題になっているのはリダイレクトしたらその先のbefore_filterで弾かれているからです。
というわけで新規ユーザーを登録したらサインイン済みになっているかどうかを検証するテストを追加しておきましょう。
spec/controller/users_controller_spec.rb の「POST create」の「登録に成功するケースの検証」のブロックに以下の検証コードを追加します。

it "新規登録成功時はサインイン済みとすること" do
post :create, :user => @attr
controller.should be_signed_in
end
RSpecではbool値を返すメソッドに対して「be_***」という検証用メソッドを自動で追加します。ここではsigined_inメソッドがtrueを返すことを期待しています。
このテストコードにより無事、NGが検出されるようになりました。
では対応しましょう。
といってもapp/controller/users_controller.rb のcreateアクションに下記赤字の1行を追加するのみです。


def create
@user = User.new(params[:user])
if @user.save
sign_in @user
flash[:success] = "登録に成功しました"
redirect_to @user
else
@title = "新規登録"
@user.password = ""
@user.password_confirmation =""
render 'new'
end
end
これで新規登録と同時にサインインした状態になるようにできました。