ユーザー認証のテスト(6) -- スタブを外す

2013/09/09

6回に渡ってユーザー認証機能の基本部分を実装してきました。今回はその仕上げです。

ユーザー認証のテスト(3) -- スタブの活用で、クラスメソッド Customer.authenticate をスタブ(常に特定の値を返す仮のメソッド)として定義しました。ユーザーインターフェースのためのテストをとりあえず通すためです。

すでに我々は本物の Customer.authenticate メソッドを手に入れたので、今やスタブを外す時です。

スタブを外す

現状のテストコード spec/features/login_and_logout_spec.rb は次のようになっています:

require 'spec_helper'

describe 'ログイン' do
  specify 'ユーザー認証成功' do
    Customer.stub(:authenticate).and_return(FactoryGirl.create(:customer))
    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

これを次のように変更します:

require 'spec_helper'

describe 'ログイン' do
  before { create(:customer, username: 'taro', password: 'correct_password') }

  specify 'ユーザー認証成功' do
    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
    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

3行目が追加され、各エグザンプルの1行目にあった Customer.stub メソッドの呼び出しが削除されています。

追加された before メソッドは、エグザンプルグループ内のすべてのエグザンプル共通の事前作業を定義します。各エグザンプルが実行される直前に before ブロック内のコードが毎回実行されるのです。ここでは、特定のユーザー名とパスワードを持つ顧客のレコードをデータベースに投入しています。

テストが通ることを確認してください。

$ bin/rspec spec/features/login_and_logout_spec.rb
..

Finished in 0.63329 seconds
2 examples, 0 failures

Randomized with seed 60662

うまく行きましたね!

ここまでのおさらい

ユーザー認証機能の開発はまだ続きますが、ここまでの流れをおさらいしておきましょう。

こんな風に進みましたね:

  1. ユーザーインターフェースのテストを作り、テストを通す。
    1. spec/features/login_and_logout_spec.rb を作成。
    2. ログインフォームのHTMLテンプレートを作成。
    3. POST /login のルーティングを設定。
    4. sessions#create アクションを仮実装(単にトップページにリダイレクト)。
    5. sessions#create アクションの実装を進める(ログイン失敗メッセージの表示)。
    6. sessions#create アクションの実装を進める(未実装の Customer.authenticate メソッドを呼ぶ)。
    7. Customer.authenticate メソッドをスタブ化し、テストを通す。
  2. Customer.authenticate メソッドのテストを作り、テストを通す。
    1. spec/models/customer_spec.rb に、エグザンプルグループ .authenticate を追加。
    2. ユーザー認証成功の場合についてエグザンプルを書き、テストを通す。
      1. customers テーブルに username カラムを追加。
      2. Customer モデルに password 属性を追加。
      3. Customer.authenticate メソッドの仮実装(ユーザー名だけで認証)。
    3. ユーザー認証失敗の場合についてエグザンプルを書き、テストを通す。
      1. customers テーブルに password_digest カラムを追加。
      2. Customer.authenticate メソッドの実装を進める(パスワードを平文で保存)。
  3. Customer#password= メソッドのテストを作り、テストを通す。
    1. spec/models/customer_spec.rb に、エグザンプルグループ #password= を追加。
    2. パスワード情報の保存に関するエグザンプルを書く。
    3. bcrypt-ruby を導入し、テストを通す。
  4. Customer.authenticate メソッドに関するエグザンプルを追加し、テストを通す。
  5. ユーザーインターフェースのテストからスタブを外す

注目すべきは「仮実装」と「スタブ」という言葉です。どちらもテストを通すのに必要最小限のコードを書くことを意味します。エグザンプルの追加とメソッドの仮実装/スタブ化を繰り返しながら、最終的には複雑な機能を持つクラス/メソッドを完成させています。

Outside-In の原則が貫かれていることにも注目しましょう。まず、最も外側のユーザーインターフェースから実装を始めています(1)。次に、ユーザー認証機能の本体である Customer.authenticate メソッドの実装に移ります(2)。そして、パスワード情報の保存という最も内側の機能を作ります(3)。ここに至り、逆の順路をたどって外側に戻っていきます。まず、Customer.authenticate メソッドの仕上げをします(4)。最後にユーザーインターフェースのテストからスタブを外します(5)。もはや「仮実装」や「スタブ」の状態で残っているメソッドは存在しません。アプリケーションとして動くことが確かめられたのです。

つまり、Outside-In というのは単に「外側から内側へ」と実装を進めるということではありません。「外側から内側へ、そして再び外側へ」という開発の流れを表現する言葉なのです。

次回は

次回からは、ユーザー認証機能の付加的な仕様である「ログインポイントの付与」のテストと実装を行っていきます。では、また。