ユーザー認証のテスト(3) -- スタブの活用
2013/09/02
前回は、ユーザー認証が失敗するシナリオについてテストを通しました。
今回は、ユーザー認証が成功するシナリオについて、一歩一歩実装を進めていきます。
sessions#create
アクションの仮実装
現在の sessions#create
アクションのコードは次の通りです:
def create if false # 未実装 else flash.alert = 'ユーザー名またはパスワードが正しくありません。' end redirect_to :root end
拙著『改訂新版 基礎Ruby on Rails』で Rails プログラミングを学んだ方は、おそらく次のようなコードを書きたくなるのではないでしょうか。
def create if customer = Customer.authenticate(params[:username], params[:password]) session[:customer_id] = customer.id else flash.alert = 'ユーザー名またはパスワードが正しくありません。' end redirect_to :root end
いくつかの理由により将来的にこのコードを変更するつもりですが、いったんこれを採用しましょう。
続いて、app/views/top/index.html.erb
を次のように変更します:
<h1>Top#index</h1> <p>Hello World!</p> <%= render 'login_form' unless session[:customer_id] %>
セッションオブジェクトの :customer_id
キーに値がセットされていない場合のみ、ログインフォームを表示するようにしました。
Customer
クラスの authenticate
メソッドが未実装であることを除けば、これで良さそうです。
で、テストを実行すると、次のような結果となります:
Failures: 1) ログイン ユーザー認証成功 Failure/Error: click_button 'ログイン' NoMethodError: undefined method `authenticate' for #<Class:0xb7fe3f3c> # ./app/controllers/sessions_controller.rb:3:in `create' # ./spec/features/login_and_logout_spec.rb:9:in `block (3 levels) in <top (required)>' # ./spec/features/login_and_logout_spec.rb:6:in `block (2 levels) in <top (required)>' 2) ログイン ユーザー認証失敗 Failure/Error: click_button 'ログイン' NoMethodError: undefined method `authenticate' for #<Class:0xb7fe3f3c> # ./app/controllers/sessions_controller.rb:3:in `create' # ./spec/features/login_and_logout_spec.rb:19:in `block (3 levels) in <top (required)>' # ./spec/features/login_and_logout_spec.rb:16:in `block (2 levels) in <top (required)>'
正しく「authenticate
メソッドが未定義」というエラーが出ています。
メソッドスタブ、あるいはスタブ
さて、このテストを通すには Customer.authenticate
メソッドを実装する必要があります。しかし、本物のメソッドを実装するには、いくつもの工程を踏まなければなりません。ユーザー名やパスワードダイジェストを記録するためのカラムを customers
テーブルに追加し、パスワードの保存と照合に関わるコードを実装する必要があります。もちろんテストも書かなくちゃいけません。類似の機能を何度も作ったことがあれば短時間で終わるかもしれませんが、書籍や Google の助けを借りずにすらすら書ける人はそんなに多くないでしょう。
いま私たちが目指していることは、ユーザー認証機能のユーザーインターフェース(要するに「見た目」)に関するテストを通すことです。Outside-In
の原則に従って、最も外側の部分を作っています。外側が未完成な間は、なるべく内側の実装に煩わされるべきではありません。この段階でパスワード情報の記録方法や照合手段について考え始めると、思考の流れが遮断されてしまいます。
あるメソッドの実装を後回しにしたい、しかしそのメソッドが存在しないとテストが通らない…。このジレンマから我々を救ってくれるのが、RSpec のメソッドスタブという機能です。通常は、短くスタブと呼びます。
まずは用例を見てください。spec/features/login_and_logout_spec.rb
を少しだけ変更しました:
require 'spec_helper' describe 'ログイン' do specify 'ユーザー認証成功' do Customer.stub(:authenticate) visit root_path within('form#new_session') do fill_in 'username', with: 'taro' fill_in 'password', with: 'correct_password' click_button 'ログイン' end expect(page).not_to have_css('form#new_session') end specify 'ユーザー認証失敗' do Customer.stub(:authenticate) visit root_path within('form#new_session') do fill_in 'username', with: 'taro' fill_in 'password', with: 'wrong_password' click_button 'ログイン' end expect(page).to have_css('p.alert', text: 'ユーザー名またはパスワードが正しくありません。') expect(page).to have_css('form#new_session') end end
5行目と16行目に Customer.stub(:authenticate)
というコードが追加されています。これは、Customer
クラスに authenticate
というスタブを定義します。スタブ(stub
)は、本物のメソッドの代わりに一時的に(テストの間だけ)定義されたメソッドを指す用語です。stub
メソッドの引数がスタブの名前となります。
英語の "stub" の原義は「(樹木の)切り株」で、他には「(歯の)折れ残り、(鉛筆やたばこの)使い残り、(入場券の)半券」といった意味があります。どういう経緯でコンピュータ用語の "stub" が生まれたのかは、調べがつきませんでした。ご存じの方がいらっしゃいましたら、ぜひ教えてください。
スタブを定義した結果、テストの結果は次のように変化します:
Failures: 1) ログイン ユーザー認証成功 Failure/Error: expect(page).not_to have_css('form#new_session') Capybara::ExpectationNotMet: expected not to find css "form#new_session", found 1 match: "ユーザー名 パスワード" # ./spec/features/login_and_logout_spec.rb:12:in `block (2 levels) in <top (required)>'
確かに NoMethodError
は出なくなりました。
スタブの戻り値を指定する
次のステップは、「ユーザー認証成功」のシナリオにおいては、Customer.authenticate
メソッドが Customer
クラスのインスタンスを返すようにすることです。
それはとても簡単です。spec/features/login_and_logout_spec.rb
の5行目を次のように修正してください。
Customer.stub(:authenticate).and_return(Customer.new)
and_return
は、スタブの戻り値を指定するメソッドです。
さあ、テストの結果はどうなるでしょうか:
Failures: 1) ログイン ユーザー認証成功 Failure/Error: expect(page).not_to have_css('form#new_session') Capybara::ExpectationNotMet: expected not to find css "form#new_session", found 1 match: "ユーザー名 パスワード" # ./spec/features/login_and_logout_spec.rb:12:in `block (2 levels) in <top (required)>'
あれれ、変化がないですね。なぜでしょうか。
FactoryGirl.create
何が起きているのかデバッグコードを埋め込んで調べてみましょう。app/controllers/sessions_controller.rb
の3行目に p customer
を追加して…
def create if customer = Customer.authenticate(params[:username], params[:password]) p customer session[:customer_id] = customer.id else flash.alert = 'ユーザー名またはパスワードが正しくありません。' end redirect_to :root end
テストを実行すると、テストの結果が表示される前にターミナルに変数 customer
の中身が表示されます。
#<Customer id: nil, family_name: nil, given_name: nil, family_name_kana: nil, given_name_kana: nil, created_at: nil, updated_at: nil>
ああ、分かりましたね。Customer.new
で作ったオブジェクトはデータベースに保存されていないので id
がセットされていないのです。
FactoryGirl に再登場願いましょう。spec/features/login_and_logout_spec.rb
の5行目を次のように修正してください。
Customer.stub(:authenticate).and_return(FactoryGirl.create(:customer))
以前、Customer
モデルのバリデーション周りのテストを書いた際には、データベースに保存する必要がなかったので、FactoryGirl.build
を使っていましたが、今回は FactoryGirl.create
でデータベースに保存します。
これでテストが通ります。
$ bin/rspec spec/features/login_and_logout_spec.rb .. Finished in 0.16719 seconds 2 examples, 0 failures Randomized with seed 5639
通ることを確認したら、デバッグコードを削除してください。そして、bin/rspec
コマンドを実行して、他のテストもすべて通ることを確認してください。
モック
以上のように RSpec のスタブ機能を用いると、思考の流れを妨げかねない重い実装作業を一時的にスキップして目下の実装作業を進めることができます。
RSpec 用語では、スタブを持つオブジェクト(クラスを含む)をモック(mock)と呼びます。上記の例では、Customer
クラスがモックです。
英語の "mock" は「まがいもの」という意味です。"mockup" または "mock-up" という形で「模型」という意味でも使われます。
RSpec には stub
メソッド以外にもモックを作る手段が用意されています。double
メソッドです。
このメソッドについては別の機会に紹介しますが、double
メソッドによって作られるモックは「純粋なモック(pure mock)」と呼ばれることだけ覚えておきましょう。特にどのクラスのインスタンスであるとも指定せずにモックを作りたいケースがあり、その時に利用します。
stub
メソッドで作られたモックは、「純粋なモック」と対比して「部分的なモック(partial mock)」と呼ばれます。基本的には本物なのだけれどもスタブだけが「まがいもの(mock)」である、という意味です。
コンピュータ用語の「モック」には、「テストスタブ」、「テストダブル」、「テストスパイ」、「フェイクオブジェクト」、「ダミーオブジェクト」などの類義語が多数あり、人によって微妙に語法が異なります。最も影響力を持ったのが『XUnit Test Patterns』(2007)を書いたGerard Meszarosによる分類です。Martin Fowler は彼の議論を整理して、最も包括的な用語として「テストダブル」を挙げ、それを「ダミー」、「フェイク」、「スタブ」、「スパイ」、「モック」に分類しています(TestDouble)。彼らの定義によれば、モックは特別な存在です。あるオブジェクトがテストの間にどのようなメッセージ(メソッドコール)を受信するかを調べるために事前に仕様を与えられたテストダブルが「モック」です。モックはテストが完了した時にメッセージ受信の記録と仕様を照らし合わせ、合致しなければ例外を投げます。しかし、RSpec の現在の開発メンバーたちは、「モック」という用語を Meszaros/Fowler の「テストダブル」と同じような包括的・総称的な意味で用いています。そして、メソッドコールの記録をチェックする機能を、モックの必須条件ではなく単なる一つの側面として捉えています。
次回は
次回の「ユーザー認証のテスト(4)」からは、Customer.authenticate
メソッドの実装に入ります。では、また。