Ruby on Rails チュートリアル

実例を使ってRailsを学ぼう

Michael Hartl (マイケル・ハートル)

第2版 目次

  1. 第1章 ゼロからデプロイまで
    1. 1.1はじめに
      1. 1.1.1読者の皆さまへ
      2. 1.1.2 Railsとスケールについて
      3. 1.1.3この本における取り決め
    2. 1.2 さっそく動作させる
      1. 1.2.1開発環境
        1. IDE
        2. テキストエディタとコマンドライン
        3. ブラウザ
        4. 使用するツールについて
      2. 1.2.2Ruby、RubyGems、Rails、Git
        1. Railsインストーラ (Windows)
        2. Gitのインストール
        3. Rubyのインストール
        4. RubyGemsのインストール
        5. Railsのインストール
      3. 1.2.3最初のアプリケーション
      4. 1.2.4 Bundler
      5. 1.2.5 rails server
      6. 1.2.6Model-view-controller (MVC)
    3. 1.3Gitによるバージョン管理
      1. 1.3.1インストールとセットアップ
        1. 初めてのシステムセットアップ
        2. 初めてのリポジトリセットアップ
      2. 1.3.2追加とコミット
      3. 1.3.3Gitのメリット
      4. 1.3.4 GitHub
      5. 1.3.5ブランチ (branch)、変更 (edit)、 コミット (commit)、マージ (merge)
        1. ブランチ (branch)
        2. 変更 (edit)
        3. コミット (commit)
        4. マージ (merge)
        5. プッシュ (push)
    4. 1.4デプロイする
      1. 1.4.1 Herokuのセットアップ
      2. 1.4.2 Herokuにデプロイする (1)
      3. 1.4.3 Herokuにデプロイする (2)
      4. 1.4.4 Heroku コマンド
    5. 1.5 最後に
  2. 第2章デモアプリケーション
    1. 2.1 アプリの計画
      1. 2.1.1ユーザーのモデル設計
      2. 2.1.2マイクロポストのモデル設計
    2. 2.2Users リソース
      1. 2.2.1ユーザーページを探検する
      2. 2.2.2 MVCの挙動
      3. 2.2.3Users リソースの欠点
    3. 2.3Microposts リソース
      1. 2.3.1マイクロポストのページを探検する
      2. 2.3.2マイクロポストをマイクロにする
      3. 2.3.3ユーザーとマイクロポストをhas_manyで関連づける
      4. 2.3.4継承の階層
      5. 2.3.5デモアプリケーションのデプロイ
    4. 2.4最後に
  3. 第3章ほぼ静的なページの作成
    1. 3.1静的ページ
    2. 3.2最初のテスト
      1. 3.2.1テスト駆動開発
      2. 3.2.2ページの追加
        1. 赤 (Red)
        2. 緑 (Green)
        3. リファクタリング
    3. 3.3少しだけ動的なページ
      1. 3.3.1タイトル変更をテストする
      2. 3.3.2タイトルのテストをパスさせる
      3. 3.3.3埋め込みRuby
      4. 3.3.4レイアウトを使って重複を解消する
    4. 3.4最後に
    5. 3.5演習
    6. 3.6高度なセットアップ
      1. 3.6.1bundle execを追放する
        1. RVM Bundler の統合
        2. binstubsオプション
      2. 3.6.2Guardによるテストの自動化
      3. 3.6.3Spork を使ったテストの高速化
        1. GuardにSporkを導入する
      4. 3.6.4Sublime Text上でテストする
  4. 第4章 Rails風味のRuby
    1. 4.1動機
    2. 4.2文字列(string)とメソッド
      1. 4.2.1コメント
      2. 4.2.2文字列
        1. 出力
        2. シングルクォート内の文字列
      3. 4.2.3オブジェクトとメッセージ受け渡し
      4. 4.2.4メソッドの定義
      5. 4.2.5 title ヘルパー、再び
    3. 4.3他のデータ構造
      1. 4.3.1配列と範囲演算子
      2. 4.3.2ブロック
      3. 4.3.3ハッシュとシンボル
      4. 4.3.4 CSS、再び
    4. 4.4 Ruby におけるクラス
      1. 4.4.1コンストラクタ
      2. 4.4.2クラス継承
      3. 4.4.3組込みクラスの変更
      4. 4.4.4コントローラクラス
      5. 4.4.5ユーザークラス
    5. 4.5最後に
    6. 4.6演習
  5. 第5章レイアウトを作成する
    1. 5.1構造を追加する
      1. 5.1.1ナビゲーション
      2. 5.1.2BootstrapとカスタムCSS
      3. 5.1.3パーシャル (partial)
    2. 5.2SassとAsset Pipeline
      1. 5.2.1Asset Pipeline
        1. アセットディレクトリ
        2. マニフェストファイル
        3. プリプロセッサエンジン
        4. 本番環境での効率性
      2. 5.2.2素晴らしい構文を備えたスタイルシート
        1. ネスト
        2. 変数
    3. 5.3レイアウトのリンク
      1. 5.3.1 ルートのテスト
      2. 5.3.2 Railsのルート
      3. 5.3.3名前付きルート
      4. 5.3.4RSpecを洗練させる
    4. 5.4ユーザー登録: 最初のステップ
      1. 5.4.1Usersコントローラ
      2. 5.4.2 ユーザー登録用URL
    5. 5.5最後に
    6. 5.6演習
  6. 第6章ユーザーのモデルを作成する
    1. 6.1 Userモデル
      1. 6.1.1データベースの移行
      2. 6.1.2modelファイル
      3. 6.1.3ユーザーオブジェクトを作成する
      4. 6.1.4ユーザーオブジェクトを検索する
      5. 6.1.5ユーザーオブジェクトを更新する
    2. 6.2ユーザーを検証する
      1. 6.2.1最初のユーザーテスト
      2. 6.2.2プレゼンスを検証する
      3. 6.2.3長さを検証する
      4. 6.2.4フォーマットを検証する
      5. 6.2.5一意性を検証する
        1. 一意性の警告
    3. 6.3セキュアなパスワードを追加する
      1. 6.3.1暗号化されたパスワード
      2. 6.3.2パスワードと確認
      3. 6.3.3ユーザー認証
      4. 6.3.4ユーザーがセキュアなパスワードを持っている
      5. 6.3.5ユーザーを作成する
    4. 6.4最後に
    5. 6.5演習
  7. 第7章ユーザー登録
    1. 7.1ユーザーを表示する
      1. 7.1.1デバッグとRails環境
      2. 7.1.2ユーザーリソース
      3. 7.1.3ファクトリーを使用してユーザー表示ページをテストする
      4. 7.1.4gravatar画像とサイドバー
    2. 7.2ユーザー登録フォーム
      1. 7.2.1ユーザー登録のためのテスト
      2. 7.2.2form_forを使用する
      3. 7.2.3フォームHTML
    3. 7.3ユーザー登録失敗
      1. 7.3.1正しいフォーム
      2. 7.3.2 Strong Parameters
      3. 7.3.3ユーザー登録のエラーメッセージ
    4. 7.4ユーザー登録成功
      1. 7.4.1登録フォームの完成
      2. 7.4.2flash
      3. 7.4.3実際のユーザー登録
      4. 7.4.4 SSLを導入して本番環境をデプロイする
    5. 7.5最後に
    6. 7.6演習
  8. 第8章サインイン、サインアウト
    1. 8.1セッション、サインインの失敗
      1. 8.1.1Sessionコントローラ
      2. 8.1.2サインインをテストする
      3. 8.1.3サインインのフォーム
      4. 8.1.4確認フォームを送信する
      5. 8.1.5フラッシュメッセージを表示する
    2. 8.2サインイン成功
      1. 8.2.1[このアカウント設定を保存する]
      2. 8.2.2正しいsign_inメソッド
      3. 8.2.3現在のユーザー
      4. 8.2.4レイアウトリンクを変更する
      5. 8.2.5ユーザー登録と同時にサインインする
      6. 8.2.6サインアウトする
    3. 8.3Cucumberの紹介 (オプション)
      1. 8.3.1インストールと設定
      2. 8.3.2フィーチャーとステップ
      3. 8.3.3対比: RSpecのカスタムマッチャー
    4. 8.4最後に
    5. 8.5演習
  9. 第9章 ユーザーの更新・表示・削除
    1. 9.1ユーザーを更新する
      1. 9.1.1編集フォーム
      2. 9.1.2編集の失敗
      3. 9.1.3編集の成功
    2. 9.2認可
      1. 9.2.1ユーザーのサインインを要求する
      2. 9.2.2正しいユーザーを要求する
      3. 9.2.3フレンドリーフォワーディング
    3. 9.3すべてのユーザーを表示する
      1. 9.3.1ユーザーインデックス
      2. 9.3.2サンプルのユーザー
      3. 9.3.3ページネーション
      4. 9.3.4パーシャルのリファクタリング
    4. 9.4ユーザーを削除する
      1. 9.4.1管理ユーザー
        1. Strong Parameters、再び
      2. 9.4.2 destroyアクション
    5. 9.5最後に
    6. 9.6演習
  10. 第10章ユーザーのマイクロポスト
    1. 10.1Micropostモデル
      1. 10.1.1基本的なモデル
      2. 10.1.2最初の検証
      3. 10.1.3User/Micropostの関連付け
      4. 10.1.4マイクロポストを改良する
        1. デフォルトのスコープ
        2. Dependent: destroy
      5. 10.1.5コンテンツの検証
    2. 10.2マイクロポストを表示する
      1. 10.2.1ユーザー表示ページの拡張
      2. 10.2.2マイクロポストのサンプル
    3. 10.3マイクロポストを操作する
      1. 10.3.1アクセス制御
      2. 10.3.2マイクロポストを作成する
      3. 10.3.3フィードの原型
      4. 10.3.4マイクロポストを削除する
    4. 10.4最後に
    5. 10.5演習
  11. 第11章ユーザーをフォローする
    1. 11.1Relationshipモデル
      1. 11.1.1データモデルの問題 (および解決策)
      2. 11.1.2User/relationshipの関連付け
      3. 11.1.3検証
      4. 11.1.4フォローしているユーザー
      5. 11.1.5フォロワー
    2. 11.2フォローしているユーザー用のWebインターフェイス
      1. 11.2.1フォローしているユーザーのサンプルデータ
      2. 11.2.2統計とフォロー用フォーム
      3. 11.2.3「フォローしているユーザー」ページと「フォロワー」ページ
      4. 11.2.4[フォローする] ボタン (標準的な方法)
      5. 11.2.5[フォローする] ボタン (Ajax)
    3. 11.3ステータスフィード
      1. 11.3.1動機と計画
      2. 11.3.2フィードを初めて実装する
      3. 11.3.3サブセレクト
      4. 11.3.4新しいステータスフィード
    4. 11.4最後に
      1. 11.4.1サンプルアプリケーションの機能を拡張する
        1. 返信機能
        2. メッセージ機能
        3. フォロワーの通知
        4. パスワードリマインダー
        5. ユーザー登録の確認
        6. RSSフィード
        7. REST API
        8. 検索機能
      2. 11.4.2読み物ガイド
    5. 11.5演習

前書き

私が前にいた会社 (CD Baby) は、かなり早い段階で Ruby on Rails に一度乗り換えたのですが、残念ながらまた PHP に戻ってしまいました (詳細は私の名前を Google で検索してみてください)。そんな私ですが、Michael Hartl 氏の本を強く勧められたので、その本を使ってもう一度試してみた結果、今度は無事に Rails に乗り換えることができました。それがこの Ruby on Rails チュートリアルという本です。

私は多くの Rails 関連の本を参考にしてきましたが、真の決定版と呼べるものは本書をおいて他にありません。本書では、あらゆる手順が Rails 流で行われています。最初のうちは慣れるまでに時間がかかりましたが、この本を終えた今、ついにこれこそが自然な方式だと感じられるまでになりました。また、本書は Rails 関連の本の中で唯一、多くのプロが推奨するテスト駆動開発 (TDD: Test Driven Development) を、全編を通して実践しています。実例を使ってここまで分かりやすく解説された本は、本書が初めてでしょう。極めつけは、Git や GitHub、Heroku の実例に含めている点です。このような、実際の開発現場で使わているツールもチュートリアルに含まれているため、読者は、まるで実際のプロジェクトの開発プロセスを体験しているかのような感覚が得られるはずです。それでいて、それぞれの実例が独立したセクションになっているのではなく、そのどれもがチュートリアルの内容と見事に一体化しています。

本書は、筋道だった一本道の物語のようになっています。私自身、章の終わりにある練習問題もやりながら、この Rails チュートリアルを3日間かけて一気に読破しました*1。最初から最後まで、途中を飛ばさずに読んでください。それが、最も有益な本書の読み方です。

それでは、楽しんでお読み下さい!

デレックシバーズ (Derek Sivers) (sivers.org) CD Baby 創始者

  1. 3日間で読破するのは異常です! 実際には3日以上かけて読むのが一般的です。

謝辞

Ruby on Rails チュートリアルは、私の以前の著書「RailsSpace」と、その時の共著者の Aurelius Prochazka から多くのことを参考にさせてもらっています。Aure には、RailsSpace での協力と本書への支援も含め、感謝したいと思います。また、RailsSpaceRails チュートリアルの両方の編集を担当して頂いた Debra Williams Cauley 氏にも謝意を表したく思います。彼女が野球の試合に連れて行ってくれる限り、私は本を書き続けるでしょう。

私にインスピレーションと知識を与えてくれた Rubyist の方々にも感謝したいと思います: David Heinemeier Hansson、 Yehuda Katz、 Carl Lerche、 Jeremy Kemper、 Xavier Noria、 Ryan Bates、 Geoffrey Grosenbach、 Peter Cooper、 Matt Aimonetti、 Gregg Pollack、 Wayne E. Seguin、 Amy Hoy、 Dave Chelimsky、 Pat Maddox、 Tom Preston-Werner、 Chris Wanstrath、 Chad Fowler、 Josh Susser、 Obie Fernandez、 Ian McFarland、 Steven Bristol、 Pratik Naik、 Sarah Mei、 Sarah Allen、 Wolfram Arnold、 Alex Chaffee、 Giles Bowkett、 Evan Dorn、 Long Nguyen、 James Lindenbaum、 Adam Wiggins、 Tikhon Bernstam、 Ron Evans、 Wyatt Greene、 Miles Forrest、 Pivotal Labs の方々、 Heroku の方々、 thoughtbot の方々、 そして GitHub の方々、ありがとうございました。最後に、ここに書ききれないほど多くの読者からバグ報告や提案を頂きました。彼ら/彼女らのおかげで、本書を可能な限り良い本に仕上げることが出来ました。

著者

マイケルハートル (Michael Hartl) は、「Ruby on Rails チュートリアル」という、Ruby on Rails を使って初めて Web アプリケーションを開発する際に最もよく参考にされる本の著者です。以前は、(今ではすっかり古くなってしまいましたが)「RailsSpace」という本の執筆および開発に携わったり、また、 一時人気を博した Ruby on Rails ベースのソーシャルネットワーキングプラットフォーム「Insoshi」の開発にも携わっていました。なお、2011年には、Rails コミュニティへの高い貢献が認められて、Ruby Hero Award を受賞しました。ハーバード大学卒業後、カリフォルニア工科大学物理学博士号を取得し、起業プログラム Y Combinator の卒業生でもあります。

著作権とライセンス

Ruby on Rails チュートリアル: 実例を使って Rails を学ぼう. Copyright © 2013 by Michael Hartl.

Ruby on Rails チュートリアル内の全てのソースコードは、MIT ライセンスおよび Beerware ライセンスの元で提供されています。(原文: All source code in the Ruby on Rails Tutorial is available jointly under the MIT License and the Beerware License.)

(訳注: "All source code" とはRailsチュートリアルの題材であるSNSのソースコードを指します。Railsチュートリアルという教材は上記ライセンスの元で提供されていないのでご注意ください)

(The MIT License)

Copyright (c) 2013 Michael Hartl

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR
A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
/*
 * ----------------------------------------------------------------------------
 * "THE BEER-WARE LICENSE" (Revision 42):
 * Michael Hartl wrote this code. As long as you retain this notice you
 * can do whatever you want with this stuff. If we meet some day,
 * and you think this stuff is worth it, you can buy me a beer in return.
 * ----------------------------------------------------------------------------
 */

第8章サインイン、サインアウト

第7章でWebサイトでの新規ユーザー登録が行えるようになりましたので、今度はユーザーがサインインとサインアウトを行えるようにしましょう。これにより、サインインの状態と現在のユーザーidに応じて動作を変更できるようになります。たとえば、この章では、サイトのヘッダー部分にサインイン/サインアウトのリンクとプロファイルへのリンクを表示するようにします。第10章では、サインインしたユーザーidを使用して、そのユーザーに関連付けられたマイクロポストを作成します。また第11章では、現在のユーザーが他のユーザーをフォローして、マイクロポストのフィードを受け取ることができるようにします。

ユーザーがサインインすることでセキュリティモデルも実装され、サインインしているユーザーidに基づいて、特定のページへのアクセスを制限することもできます。第9章でも説明しますが、たとえばサインインしたユーザーのみがユーザー情報編集ページにアクセスできるようにします。サインインシステムを使用して、管理者ユーザーに特別な権限を与えることもできます。たとえば (第9章で導入する) データベースからユーザーを削除する機能があります。

コアとなる認証機能を実装した後は、少々回り道して、振舞駆動開発 (8.3) で有名なCucumberというツールを試してみます。特に、2つの開発手法を比較するために、RSpecによる結合テストの組み合わせをCucumberで再実装します。

前章同様に、トピックブランチで作業してから、最後に更新をマージします。

$ git checkout -b sign-in-out

8.1セッション、サインインの失敗

セッションとは、2つのコンピュータの間、たとえばクライアント側のブラウザとサーバーで動作しているRailsとの間の、半永続的な接続のことです。ここでは、”サインイン” の共通パターンを実装するためにセッションを使用します。ここで、Webの世界にはセッションの振る舞いを表現するためのいくつかの異なったモデルがあります。ブラウザを閉じるとセッションを終了する「忘却モデル」、[パスワードを保存する] チェックボックスを使用してセッションを継続する「継続モデル」、ユーザーが明示的にサインアウトするまでセッションを継続する「永続モデル」などです1。ここでは、最後の永続モデルを採用することにします。ユーザーがサインインすると、ユーザーが明示的にサインアウトするまでサインインの状態を永続させます (8.2.1では、“永続” が実際にはどのぐらいの時間であるかをお見せします)。

セッションは、RESTfulなリソースとして作成しておくと便利です。たとえばサインインページをnewセッションで、サインインをcreateセッションで、サインアウトをdestroyセッションでそれぞれ扱います。Usersリソースのように (Usersモデルを経由して) データベースをバックエンドに持つリソースとは異なり、このSessionsリソースではcookiesを使用します。cookiesとは、ブラウザに保存される小さなテキストデータです。サインイン関連の作業の大半は、このcookiesをベースにして認証システムを構築することになります。この節と次の節では、セッション機能を作成する準備として、Sessionコントローラ、サインイン用のフォーム、そしてこれらに関連するコントローラのアクションを作成します。次の8.2では、cookies管理のために必要なコードを追加して、ユーザーがサインインするための仕組みを完成させます。

8.1.1 Sessionコントローラ

サインインとサインアウトの要素は、Sessionsコントローラの特定のRESTアクションにそれぞれ対応します。サインインのフォームは、この節のnewアクションで処理されます。実際のサインイン処理は、createアクションにPOSTリクエストが送信されたときに処理されます (8.1および8.2) 。サインアウトは、destroyアクションにDELETEリクエストを送信することで処理されます(8.2.6)。(表7.1のHTTPメソッドとRESTアクションの関連付けを思い出してください。) それでは最初に、Sessionsコントローラと認証システムをテストする結合テストを作成します。

$ rails generate controller Sessions --no-test-framework
$ rails generate integration_test authentication_pages

7.2で作成したユーザー登録ページのモデルを元に、新しいセッションを確立するためのフォームを図8.1のモックアップに従って作成します。

signin_mockup_bootstrap
図8.1サインインフォームのモックアップ。(拡大)

このサインインページは、signin_pathによって仮に定義されるURLでアクセス可能になります。リスト8.1のように、最初は最小限のテストから始めます (リスト7.6のユーザー登録ページのコードと似ているので、比較してみてください)。

リスト8.1 セッションのnewアクションとビューをテストする。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do

  subject { page }

  describe "signin page" do
    before { visit signin_path }

    it { should have_content('Sign in') }
    it { should have_title('Sign in') }
  end
end

以下のテストは、この時点では失敗するはずです。

$ bundle exec rspec spec/
 

リスト8.1のテストがパスするためには、サインインページ用にカスタマイズした名前付きルートを使用して、Sessionsリソースのルーティングを定義する必要があります。このサインインページはSessionコントローラのnewアクションと対応しています。Usersリソースの場合と同様に、resourcesメソッドを使用して通常のRESTfulなルーティングを設定することができます。

resources :sessions, only: [:new, :create, :destroy]

セッションを編集したりユーザーに表示したりする必要がないので、resourcesメソッドに:onlyオプションを追加し、newcreatedestroyの3つのアクションのみを有効にします。サインインとサインアウト用のルーティングを含む完全なコードをリスト8.2に示します。

リスト8.2 リソースを追加して標準のRESTfulアクションを設定する。
config/routes.rb
SampleApp::Application.routes.draw do
  resources :users
  resources :sessions, only: [:new, :create, :destroy]
  root  'static_pages#home'
  match '/signup',  to: 'users#new',            via: 'get'
  match '/signin',  to: 'sessions#new',         via: 'get'
  match '/signout', to: 'sessions#destroy',     via: 'delete'
  .
  .
  .
end

注: サインアウトのルーティングにあるvia: ’delete’は、このアクションが HTTPのDELETEリクエストによって呼び出されることを示しています。

リスト8.2で定義されたリソースは、表8.1に示したように、表7.1とよく似たURLとアクションを提供します。注:サインインとサインアウトのアクションのルーティングはカスタムで設定しますが、セッションの生成アクションへのルーティングはデフォルトを使います (i.e., [resource name]_path).

HTTPリクエストURL名前付きルートアクション用途
GET/signinsignin_pathnew新しいセッション用 (サインイン)
POST/sessionssessions_pathcreate新しいセッションを作成する
DELETE/signoutsignout_pathdestroyセッションを削除する (サインアウト)
表8.1リスト8.2のセッションルールによって提供されるRESTfulなルート。

次に、リスト8.1のテストがパスするようにするために、リスト8.3に示したようにnewアクションをSessionsコントローラに追加します (後で使えるようcreateアクションとdestroyアクションも定義しておきます) 。

リスト8.3 最初のSessionsコントローラ。
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
  end

  def destroy
  end
end

最後に、サインインページを新規に定義します。このページは新規セッション用なので、今から作成するサインインページをapp/views/sessions/new.html.erbに置くことに注目してください。リスト8.4に示したように、今の時点ではタイトルと一番上のヘッダ部分のみを定義します。

リスト8.4 最初のサインインビュー。
app/views/sessions/new.html.erb
<% provide(:title, "Sign in") %>
<h1>Sign in</h1>

これにより、リスト8.1のテストが青信号 (成功) になります。これで実際のサインインフォームを作成する準備ができました。

$ bundle exec rspec spec/

8.1.2サインインをテストする

図8.1図7.11を比較すると、サインインフォーム (新規セッションフォーム) の外観は、4つあったフィールドがメールアドレスとパスワードの2つになったこと以外は、ユーザー登録フォームによく似ていることに気付くと思います。ユーザー登録フォームのときと同様に、サインインフォームでもCapybaraを使ってフォームに値を入力し、ボタンをクリックするテストを行うことができます。

テストを作成していると、私たちはアプリケーションをさまざまな側面から設計することを強いられます。これはテスト駆動開発の素晴らしい副次的効果のひとつです。図8.2のモックアップで示されているように、最初はサインインの入力に誤りがあった場合のテストから作成します。

signin_failure_mockup_bootstrap
図8.2サインイン失敗時のモックアップ。(拡大)

図 8.2に示されているように、サインインで入力した情報に誤りがあったときは、サインインページをもう一度表示して、エラーメッセージを出力します。ここではエラーをフラッシュメッセージとして表示するので、以下のようにテストできます。

it { should have_selector('div.alert.alert-error', text: 'Invalid') }

上のコードでは、リスト7.32で導入したCapybaraのhave_selectorメソッドを使用しています (第7章の演習)。have_selectorメソッドは、特定のセレクタ要素 (HTMLタグなど) があるかどうかをチェックします。なお、Capybara 2.0では画面に表示される要素しかチェックできません。セレクタ要素(つまりタグ)は以下のように指定します。

div.alert.alert-error

上のコードはdivタグがあるかどうかをチェックします。上のコードのドットはCSSのクラスを意味することを思い出してください (5.1.2) 。つまりこのテストでは、"alert"クラスと"alert-error"クラスのdivタグを対象にしていることがわかります。また、エラーメッセージに"Invalid"という単語が含まれていることもテストします。これらを合わせると、次のフォームの要素を探しだしてテストが行われます。

<div class="alert alert-error">Invalid...</div>

リスト 8.5のコードは、タイトルとフラッシュメッセージをまとめてテストします。ただしこのテストは、実はある重要な点を欠いています。これは8.1.5で改善します。

リスト8.5 サインイン失敗時のテスト。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "signin" do
    before { visit signin_path }

    describe "with invalid information" do
      before { click_button "Sign in" }

      it { should have_title('Sign in') }
      it { should have_selector('div.alert.alert-error', text: 'Invalid') }
    end
  end
end

サインインに失敗した時のテストができたので、次はサインインに成功した場合のテストを作成しましょう。ユーザープロファイルページが表示されること (ページタイトルがユーザー名になっていること)、サイトのナビゲーションに次の3つの変更が加えられていることを確認するよう、テストを変更します。

  1. プロファイルページヘのリンクの表示
  2. [Sign out] リンクの表示
  3. [Sign in] リンクの非表示

([Setting] リンクについては9.1で、[Users] リンクについては9.3でそれぞれテストします) 。これらの変更を加えたモックアップを図8.3に示しました2。サインアウトとプロファイルページヘのリンクは、[Account] メニューにドロップダウンで表示されます。Bootstrapを使用してドロップダウンメニューを作成する方法については8.2.4で説明します。

signin_success_mockup_bootstrap
図8.3サインインに成功後表示されるユーザープロファイルのモックアップ。(拡大)

サインインに成功したときのテストコードをリスト8.6に示しました。

リスト8.6 サインインに成功したときのテスト。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "signin" do
    before { visit signin_path }
    .
    .
    .
    describe "with valid information" do
      let(:user) { FactoryGirl.create(:user) }
      before do
        fill_in "Email",    with: user.email.upcase
        fill_in "Password", with: user.password
        click_button "Sign in"
      end

      it { should have_title(user.name) }
      it { should have_link('Profile', href: user_path(user)) }
      it { should have_link('Sign out', href: signout_path) }
      it { should_not have_link('Sign in', href: signin_path) }
    end
  end
end

上のコードでは、Capybaraのhave_linkメソッドが導入されています。このメソッドは、リンクテキストを引数にとります。オプションとして次のように:hrefパラメータを加えると、

it { should have_link('Profile', href: user_path(user)) }

アンカータグahref (URL) 属性を追加することもできます (この例では、ユーザープロファイルへのリンク)。上のテストでは、upcaseメソッドを使用してユーザーのメールアドレスを大文字に変換することで、大文字小文字を区別しないデータベースが使用されている場合であってもユーザーを確実に検索できるように配慮してあることに注目してください。

8.1.3サインインのフォーム

テストの準備が完了したので、いよいよサインインフォームの開発に取りかかりましょう。リスト7.17のときは、以下のようにユーザー登録フォームでform_forヘルパーを使用し、ユーザーのインスタンス変数 @userを引数にとっていました。

<%= form_for(@user) do |f| %>
  .
  .
  .
<% end %>

このユーザー登録フォームとサインインフォームの主な違いは、サインインフォームにはSessionモデルがないために@user変数のような存在がないことです。 これはつまり、新しいセッションフォームを作成するときにform_forヘルパーに追加の情報を渡す必要があるということです。

form_for(@user)

Railsは上の場合、フォームのactionは/usersというURLへのPOSTであると自動的に判定しますが、セッションの場合はリソースの名前とそれに対応するURLを指定する必要があります。

form_for(:session, url: sessions_path)

(注: form_forの代わりにform_tagを使うこともでき、Railsではこの方が慣用的な方法です。しかし、ユーザー登録フォームではform_forを使用する方が一般的であり、並列構造を強調するためにもform_forを使用しました。form_tagを使ったフォームについては(8.5)の演習に回します。)

適切なform_forを使用することで、リスト7.17のユーザー登録フォームを参考にして、リスト8.7に示したようなモックアップに従ったサインインフォームを簡単に作成できます (図8.1)。

リスト8.7 サインインフォームのコード。
app/views/sessions/new.html.erb
<% provide(:title, "Sign in") %>
<h1>Sign in</h1>

<div class="row">
  <div class="span6 offset3">
    <%= form_for(:session, url: sessions_path) do |f| %>

      <%= f.label :email %>
      <%= f.text_field :email %>

      <%= f.label :password %>
      <%= f.password_field :password %>

      <%= f.submit "Sign in", class: "btn btn-large btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

ユーザーにとって便利なように、ユーザー登録ページのリンクを追加してあることに注目してください。リスト 8.7のコードで生成されるサインインフォームを図 8.4に示します。

signin_form_bootstrap
図8.4サインインフォーム (/signin)。(拡大)

これまではRailsが生成したHTMLをブラウザでたびたび確認していた方も多いと思いますが、やがてその習慣は忘れ去られ、ヘルパーが正常に動作していることを信頼するようになる日が来ることでしょう。今はいつものように(リスト 8.8)を見てみましょう。

リスト8.8 リスト8.7で生成されたサインインフォームのHTML。
<form accept-charset="UTF-8" action="/sessions" method="post">
  <div>
    <label for="session_email">Email</label>
    <input id="session_email" name="session[email]" type="text" />
  </div>
  <div>
    <label for="session_password">Password</label>
    <input id="session_password" name="session[password]"
           type="password" />
  </div>
  <input class="btn btn-large btn-primary" name="commit" type="submit"
       value="Sign in" />
</form>

リスト8.8リスト7.20を比較することで、フォーム送信後にparamsハッシュに入る値が、メールアドレスとパスワードのフィールドにそれぞれ対応したparams[:session][:email]params[:session][:password]になることを推測できることでしょう。

8.1.4確認フォームを送信する

ユーザーを作成する (ユーザー登録) 場合と同様、セッションを作成する場合 (サインイン) で最初にやることは、正しくない入力の取り扱いです。ユーザー登録のテストは既に作成済みです (リスト 8.5)。そしてサインインのアプリケーションコードは、いくつかの点においてユーザー登録のコードとわずかに異なります。最初に、フォームが送信されたとき何が起こるかを考えましょう。次に、サインインが失敗した場合に表示されるエラーメッセージを配置します (モックアップを図8.2に示しました)。次に、サインインが送信されるたびに、パスワードとメールアドレスの組み合わせが正しいかどうかを判定し、サインインに成功した場合に使用する土台 (8.2) を作成します。

最初に、Sessionsコントローラに最小限のcreateアクションを定義します (リスト8.9) 。このアクションは何も実行しませんが、newビューを表示します。/sessions/newフォームを表示し、フィールドを空白のまま [Sign in] を押して送信すると図8.5のように表示されます。

リスト8.9 Sessionsコントローラのcreateアクションの試作バージョン。
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  .
  .
  .
  def create
    render 'new'
  end
  .
  .
  .
end
initial_failed_signin_rails_3_bootstrap
図8.5リスト8.9createへのサインイン失敗結果。(拡大)

図8.5 に表示されているデバッグ情報に注目してください。8.1.3 の終わりにもヒントがありましたが、paramsハッシュはメールアドレスとパスワードを以下のように:sessionキーの下に持ちます。

---
session:
  email: ''
  password: ''
commit: Sign in
action: create
controller: sessions

ユーザー登録の場合 (図7.15) と同様、これらのパラメータはリスト4.6に示したようにネストした (入れ子になった) ハッシュになっています。特に、paramsは以下のような入れ子ハッシュになっています。

{ session: { password: "", email: "" } }

これはつまり、

params[:session]

上はそれ自体がハッシュであり、以下の要素を含んでいます。

{ password: "", email: "" }

その結果、

params[:session][:email]

上はフォームから送信されたメールアドレスであり、

params[:session][:password]

上はフォームから送信されたパスワードです。

言い換えれば、createアクションのparamsハッシュには、ユーザーの認証に必要な情報がすべて含まれているということになります。ここまでに、私たちはすでに必要なメソッドを手にしています。 Active RecordがUser.find_by_emailを提供し (6.1.4)、 has_secure_passwordauthenticateメソッドを提供しています (6.3.3)。認証に失敗したとき、authenticateの返り値はfalseになることを思い出してください。ユーザーのサインイン方法の方針をまとめると以下のようになります。

def create
  user = User.find_by(email: params[:session][:email].downcase)
  if user && user.authenticate(params[:session][:password])
    # ユーザーをサインインさせ、ユーザーページ (show) にリダイレクトする。
  else
    # エラーメッセージを表示し、サインインフォームを再描画する。
  end
end

最初の行は、送信されたメールアドレスを使用して、データベースからユーザーを取り出しています。(6.2.5ではメールアドレスはすべて小文字で保存されていたことを思い出してください。従って、ここではdowncaseメソッドを使用して、正しいメールアドレスが入力されたときに確実にマッチするようにしています)。次の行は少しわかりにくいかもしれませんが、Railsプログラミングでは定番の手法です。

user && user.authenticate(params[:session][:password])

&& (論理積) は、取得したユーザーが有効かどうかを決定するのに使用します。nilfalse以外のすべてのオブジェクトは、真偽値ではtrueになる (4.2.3) というRubyの性質を考慮すると、 組み合わせは表8.2のようになります。 表 8.2を見ると、入力されたメールアドレスを持つユーザーがデータベースに存在し、かつ入力されたパスワードがそのユーザーのパスワードである場合のみ、if文がtrueになることがわかります。

ユーザーパスワードa && b
存在しない何でもよいnil && [anything] == false
有効なユーザー誤ったパスワードtrue && false == false
有効なユーザー正しいパスワードtrue && true == true
表8.2user && user.authenticate(…)の結果の組み合わせ。

8.1.5フラッシュメッセージを表示する

7.3.2では、Userモデルのエラーメッセージを使用してユーザー登録のエラーメッセージを表示したことを思い出してください。これらのエラーメッセージは、特定のActive Recordオブジェクトに関連付けられていますが、セッションはActive Recordのモデルではないためにこの方法はここでは使用できません。代わりに、サインインに失敗したときにフラッシュメッセージを表示することにします。最初のコードをリスト8.10に示します (ここには少しだけ間違いがあります)。

リスト8.10 サインインの失敗を扱う (誤りあり)。
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーをサインインさせ、ユーザーページ (show) にリダイレクトする。
    else
      flash[:error] = 'Invalid email/password combination' # 誤りあり!
      render 'new'
    end
  end

  def destroy
  end
end

フラッシュメッセージはWebサイトのレイアウトに表示される (リスト7.27) ので、flash[:error]のメッセージは自動的に表示されます。Bootstrap CSSのおかげで適切なスタイルも与えられます (図8.6)。

failed_signin_flash_bootstrap
図8.6サインインに失敗したときのフラッシュメッセージ。(拡大)

残念ながら、本文およびリスト8.10のコメントで述べたように、このコードには誤りがあります。ページの表示には問題がないようですが、どこがおかしいのでしょうか。実は、あるリクエストに対するフラッシュメッセージの内容がすぐに消えずに残ってしまうという問題があります。リスト7.28で使用したリダイレクトのときとは異なり、renderで表示したテンプレートを再描画してもリクエストと見なされません。このため、フラッシュメッセージはひとつのリクエストに対して期待よりも長い間消えずに表示され続けてしまいます。たとえば、無効な情報を送信するとサインインページにフラッシュメッセージが設定されて表示されます (図8.6)。ここでHomeページなどの別のページへのリンクをクリックすると、これはフォームが送信されてから最初のリクエストであるため、フラッシュメッセージが移動先のページでも表示されたままになってしまいます (図8.7)。

flash_persistence_bootstrap
図8.7フラッシュメッセージが消えずに残っている例。(拡大)

フラッシュメッセージの残留問題はこのアプリケーションのバグです。この問題を修正する前に、この問題をキャッチするテストを書くのが正しいやり方です。特に、現在のサインイン失敗テストではこの問題がキャッチされずにパスしてしまいます。

$ bundle exec rspec spec/requests/authentication_pages_spec.rb \
> -e "signin with invalid information"

当然ながら、既知のバグが未修正の状態であれば、このテストはパスするべきではありません。この問題をキャッチする、失敗するテストを追加しましょう。幸い、結合テストはフラッシュメッセージ残留などの多くの問題解決において大活躍します。期待される動作は以下のテストで正確に表現されています。

describe "after visiting another page" do
  before { click_link "Home" }
  it { should_not have_selector('div.alert.alert-error') }
end

このテストは、無効なサインインデータを送信し、次にWebサイトのレイアウトにあるHomeリンクを開き、フラッシュメッセージが表示されていないことを確認します。フラッシュテストの部分を更新したテストコードをリスト8.11に示します。

リスト8.11 サインインの失敗を正しくテストするコード。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "signin" do

    before { visit signin_path }

    describe "with invalid information" do
      before { click_button "Sign in" }

      it { should have_title('Sign in') }
      it { should have_selector('div.alert.alert-error', text: 'Invalid') }

      describe "after visiting another page" do
        before { click_link "Home" }
        it { should_not have_selector('div.alert.alert-error') }
      end
    end
    .
    .
    .
  end
end

再度テストを実行すると、期待どおり失敗します。

$ bundle exec rspec spec/requests/authentication_pages_spec.rb \
> -e "signin with invalid information"

失敗するテストがパスするようになるために、flashの代わりにflash.nowを使用します。これもフラッシュメッセージをページに表示するために設計されたメソッドですが、flashの場合とは異なり、他のリクエストが発生したらすぐにメッセージを消します。正しいアプリケーションコードをリスト8.12に示します。

リスト8.12 サインインが失敗したときの正しいコード。
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーをサインインさせ、ユーザーページ (show) にリダイレクトする。
    else
      flash.now[:error] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
  end
end

これで、ユーザー情報が無効な場合のテストスイートが緑色 (成功) になりました。

$ bundle exec rspec spec/requests/authentication_pages_spec.rb \
> -e "with invalid information"

8.2サインイン成功

サインイン失敗をテストできるようにしたので、次は実際にユーザーをサインインさせましょう。この節で必要なRubyのプログラミングは、本書の中ではこれまでで最も難易度が高くなっています。どうか最後まであきらめずにがんばってください。ここからの力仕事に備えておきましょう。幸い、はじめの一歩は簡単です。Sessionsコントローラのcreateアクションはすぐできあがります。残念ながら、少々ズルもしています。

サインインの実装 (リスト8.12) はシンプルです。サインインに成功すると、sign_in関数を使用して実際にユーザーをサインインさせ、プロファイルページにリダイレクトします (リスト8.13)。なぜこれがズルなのかというと、何とsign_inはこの時点では存在していないのです。この節の残りは、この関数を完成させることに費やされます。

リスト8.13 Sessionsコントローラのcreateアクションが完成したところ (まだ動きません)。
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  .
  .
  .
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      sign_in user
      redirect_to user
    else
      flash.now[:error] = 'Invalid email/password combination'
      render 'new'
    end
  end
  .
  .
  .
end

8.2.1[このアカウント設定を保存する]

これよりサインインモデルの実装を開始します。具体的には、サインイン状態を「永続化」し、ユーザーが明示的にサインアウトしたときにのみセッションを終了します。サインイン関数そのものは伝統的なMVC (model-view-controller)) に帰着します。特に、いくつかのサインイン関数についてはコントローラとビューのどちらからも使用できるようにする必要があります。4.2.5を思い出してみてください。Rubyには、いくつもの関数をパッケージ化し、さまざまな場所でインクルードできるようにするモジュールという機能が提供されています。これを認証機能の実装に使用しましょう。認証のためにまったく新しいモジュールを作ることも可能ですが、Sessionsコントローラには既にSessionsHelperというモジュールが備わっています。さらに、ヘルパーはRailsのビューに自動的にインクルードされるので、Applicationコントローラにこのモジュールをインクルードするだけで、Sessionsヘルパー機能をコントローラ内で使用できるようになります (リスト8.14)。

リスト8.14 SessionsヘルパーモジュールをApplicationコントローラにインクルードする。
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper
end

デフォルトでは、すべてのヘルパーはビューで使用できますが、コントローラでは使用可能になっていません。Sessionsヘルパーはビューとコントローラの両方でメソッドが必要となるので、コントローラでは上のように明示的にインクルードする必要があります。

HTTPはステートレスなプロトコルであり、そのままでは状態が保存されないので、Webアプリケーションのサインインは、ページからページの移動を追跡するという方法で実装する必要があります。ユーザーのサインイン状態を保持するひとつの方法は、伝統的なRailsセッション (特殊なsession関数を使用) を使って、ユーザーIDに等しい「記憶トークン (remember token)」を保持することです。

session[:remember_token] = user.id

このsessionオブジェクトは、ユーザーidをcookiesに保持することで、ページ移動後にもユーザーidを参照できるようにしています。cookiesはブラウザが閉じられると無効になります。アプリケーションは、ページごとに以下の呼び出しを行います。

User.find(session[:remember_token])

これだけで、ユーザーを取り出すことができます。Railsでは、セッションがセキュアになるように扱っていますので、悪意のあるユーザーがidをなりすまそうとしても、Railsはセッションごとに生成される特別のセッションidによって不一致を検出します。

私たちのアプリケーション設計では、永続的なセッションを採用します。つまり、ブラウザを閉じた後にもサインイン状態を保持するということであり、サインインしたユーザーに対して何らかの恒久的な識別子を使用する必要があります。これを実現するために、ユーザーごとに一意かつ安全な記憶トークンを生成し、ブラウザを閉じても無効にならない恒久的なcookiesとして登録します。

記憶トークンはユーザーに関連付けられる必要があります。また、今後使用するときのために保持しておく必要もあります。そこで、図8.8に示すUserモデルの属性に記憶トークンの保存場所を追加しましょう。

user_model_remember_token_31
図8.8remember_token属性を追加したUserモデル。

最初に、Userモデルのspecに若干の追加を行います (リスト8.15)。

リスト8.15 記憶トークン用の最初のテスト。
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:password_confirmation) }
  it { should respond_to(:remember_token) }
  it { should respond_to(:authenticate) }
  .
  .
  .
end

コマンドラインで以下のように記憶トークンを生成することで、上のテストがパスするようになります。

$ rails generate migration add_remember_token_to_users

次に、上で生成されたマイグレーションにリスト8.16のコードを記入します。ユーザーを記憶トークンで検索できるように、インデックス (コラム 6.2) をremember_tokenカラムに追加していることに注目してください。

リスト8.16 remember_tokenusersテーブルに追加したマイグレーション。
db/migrate/[ts]_add_remember_token_to_users.rb
class AddRememberTokenToUsers < ActiveRecord::Migration
  def change
    add_column :users, :remember_token, :string
    add_index  :users, :remember_token
  end
end

次に、いつものように開発データベースとテストデータベースを更新します。

$ bundle exec rake db:migrate
$ bundle exec rake db:test:prepare

この時点で、Userモデルのspecはパスするはずです。

$ bundle exec rspec spec/models/user_spec.rb

ここで、記憶トークンに何を使用するかを決める必要があります。有力な候補としてさまざまなものが考えられますが、基本的には一意性を確保できる、長くてランダムな文字列でさえあればどんなものでも良いでしょう。Ruby標準ライブラリのSecureRandomモジュールにあるurlsafe_base64メソッドならこの用途にぴったりです3。このメソッドは、A–Z、a–z、0–9、“-”、“_”のいずれかの文字 (64種類) からなる長さ16のランダムな文字列を返します (そのためbase64と呼ばれています)。生成された2つの記憶トークンが偶然一致 (衝突) する確率は、64のマイナス16乗、2のマイナス96乗 (10のマイナス29乗) にほぼ等しく、トークンが衝突することはまずありません。

ここでのねらいは、ブラウザにこのbase64トークンを保存しておき、データベースにはトークンを暗号化したものを保存することです。そして、cookiesからこのトークンを読みだして暗号化し、データベース上にある暗号化された記憶トークンと一致するものがあるかどうかを検索することにより、ユーザーを自動的にサインインさせることができます。暗号化したトークンだけをデータベースに保存する理由は、万が一データベースが不正アクセスを受けるようなことがあっても、攻撃者が記憶トークンを使用してサインインできないようにするためです。記憶トークンをさらにセキュアにするためには、新しいセッションを作成するたびにトークンを更新するのがよい方法です。この方法なら、攻撃者が盗んだcookiesを使用して本物のユーザーになりすましてサインインしようとする (セッションハイジャック) ことがあっても、ユーザーが次回サインインするときにはトークンが期限切れになります。(セッションハイジャックは、セキュリティ上の注意を呼びかけるためにこれを実演するFiresheepアプリケーションによって広く知られるようになりました。Firesheepを使用すると、公共Wi-Fiネットワーク経由で接続したときに多くの有名Webサイトの記憶トークンが丸見えになっていることがわかります。この問題を解決するには、7.4.4で説明したようにWebサイト全体をSSLで暗号化することです。)

サンプルアプリケーションでは、新規作成されたユーザーをその場でサインインさせ、その副作用として記憶トークンがついでに作成されますが、この動作に依存すべきではありません。安全を確保するために、すべてのユーザーが必ず最初から有効な記憶トークンを持つようにする必要があります。これを確実に行うには、コールバックを使用して記憶トークンを生成します。このテクニックは、6.2.5でemailの一意性の文脈で紹介しました。あのときはbefore_saveというコールバックを使用しましたが、ここではそれによく似たbefore_createというコールバックを使用して、ユーザー新規作成時に記憶トークンを設定することにします4

記憶トークンをテストするために、最初にテストユーザーを保存し (これまでは作成されても保存はされていませんでした)、次にユーザーのremember_token属性が空欄でないことを確認します。これにより、必要が生じたときにランダム文字列を変更するのに十分な柔軟性が得られます。このテストをリスト8.17に示します。

リスト8.17 記憶トークンが有効である (空欄のない) ことをテストする。
spec/models/user_spec.rb
require 'spec_helper'

describe User do

  before do
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end

  subject { @user }
  .
  .
  .
  describe "remember token" do
    before { @user.save }
    its(:remember_token) { should_not be_blank }
  end
end

上のリスト8.17で導入したitsメソッドは、itと似ていますが、itが指すテストのsubject (ここでは@user) そのものではなく、引数として与えられたその属性 (この場合は:remember_token) に対してテストを行うときに使用します。

its(:remember_token) { should_not be_blank }

つまり、上のコードは以下と等価です。

it { expect(@user.remember_token).not_to be_blank }

このアプリケーションコードでは、新しい要素をいくつか導入します。最初に、記憶トークンを作成するために以下のようにコールバックメソッドを追加します。

before_create :create_remember_token

上のコードはメソッド参照と呼ばれるもので、こうすることでRailsはcreate_remember_tokenというメソッドを探し、ユーザーを保存する前に実行するようになります (リスト6.20では、before_saveに明示的にブロックを渡していましたが、メソッド参照の方が一般にお勧めできます) 。次に、このメソッド自身はUserモデルの内部でしか使用しないので、外部のユーザーがアクセスできるようにする必要はありません。7.3.2でも触れたように、メソッド定義に以下のようにprivateキーワードを与えるのがRuby流です。

private

  def create_remember_token
    # トークンを作成する。
  end

privateキーワード以降で定義されたメソッドはすべて隠蔽されます。

$ rails console
>> User.first.create_remember_token

このため、上をコンソールで実行するとNoMethodError例外が発生します。

最後に、create_remember_tokenメソッドは、ユーザーの属性のひとつに「要素代入 (assignment)」する必要があります。この文脈では、remember_tokenの前にselfキーワードを追加する必要があります。

  def User.new_remember_token
    SecureRandom.urlsafe_base64
  end

  def User.encrypt(token)
    Digest::SHA1.hexdigest(token.to_s)
  end

  private

    def create_remember_token
      self.remember_token = User.encrypt(User.new_remember_token)
    end

上のコードにselfというキーワードがないと、要素代入によってremember_tokenという名前のローカル変数が作成されてしまうので、注意が必要です (ここで本来必要なのはローカル変数ではなく、インスタンス変数です)。この動作は、Rubyにおけるオブジェクト内部への要素代入の仕様によるものです。selfキーワードを与えることで、要素代入は正しくそのユーザーのremember_token属性を設定するようになり、その結果ユーザーが保存されるときに他の属性と一緒にこの属性もデータベースに保存されます (リスト6.20の他のbefore_saveコールバックでも、emailではなくself.emailと記述していた理由が、これでおわかりいただけたと思います)。

SHA1を使用して記憶トークンを暗号化すると、6.3.1でユーザーのパスワード暗号化に使用していたBcryptアルゴリズムよりも動作がはるかに高速である点に注目してください。8.2.2でも説明しますが、トークンの暗号化はユーザーがページを開くたびに行うので、暗号化アルゴリズムが高速であることは重要です。SHA1はBcryptと比べるとそれほどセキュアではありませんが、元となるトークンが既に16桁のランダムな文字列なので、これで十分です。このようなランダムな文字列をさらにSHA1でhexdigestしたものは、本質的にクラック不可能です (to_sメソッドを呼び出しているのは、nilトークンを扱えるようにするためです。ブラウザでnilトークンが発生することはあってはなりませんが、テスト中に発生することはありえるためです)。

encryptメソッドとnew_remember_tokenメソッドは、ユーザーのインスタンスに対して動作する必要がないので、インスタンスではなくUserクラスに属します (クラスメソッド)。また、これらのメソッドはprivate行より上にあるのでpublicになっていますが、これは8.2.3でUserモデルの外で使用する必要があるためです。

以上の実装をまとめたUserモデルをリスト8.18に示します。

リスト8.18 before_createコールバックを使用して remember_token属性を作成する。
app/models/user.rb
class User < ActiveRecord::Base
  before_save { self.email = email.downcase }
  before_create :create_remember_token
  .
  .
  .
  def User.new_remember_token
    SecureRandom.urlsafe_base64
  end

  def User.encrypt(token)
    Digest::SHA1.hexdigest(token.to_s)
  end

  private

    def create_remember_token
      self.remember_token = User.encrypt(User.new_remember_token)
    end
end

ちなみに、privateキーワード以降のコードは、強調のためcreate_remember_tokenのインデントを1段深くしてあります (経験上、こうしておくことをお勧めします)。

SecureRandom.urlsafe_base64は決して空欄にはならなくなったので、Userモデルのテストはパスするはずです。

$ bundle exec rspec spec/models/user_spec.rb

8.2.2正しいsign_inメソッド

いよいよ、最初のサインイン要素であるsign_in関数自身の実装に取りかかりましょう。既に述べたように、これから作る認証メソッドは、記憶トークンをブラウザのcookiesに保存し、ユーザーが別のページに移動する (8.2.3で実装予定) ときにそのトークンを使用してデータベース内のユーザーのレコードを見つけます。このコードをリスト8.19に示します。なお、このコードにはcurrent_userというメソッドがありますが、これは8.2.3で実装します。

リスト8.19 完全だがまだ動作しないsign_in関数。
app/helpers/sessions_helper.rb
module SessionsHelper

  def sign_in(user)
    remember_token = User.new_remember_token
    cookies.permanent[:remember_token] = remember_token
    user.update_attribute(:remember_token, User.encrypt(remember_token))
    self.current_user = user
  end
end

ここでは以下の手順で進めます。1) トークンを新規作成する。2) 暗号化されていないトークンをブラウザのcookiesに保存する。3) 暗号化したトークンをデータベースに保存する。4) 与えられたユーザーを現在のユーザーに設定する (8.2.3)。上のコードで、update_attributeを使用してトークンを保存している点に注目してください。6.1.5でも簡単に説明しましたが、このメソッドを使用すれば検証をすり抜けて単一の属性を更新することができます。ページの移動ではユーザーのパスワードやパスワード確認は与えられないので、このメソッドを使用する必要があります。

リスト8.19ではRailsが提供するcookiesユーティリティも使用しています。これを使用することで、ブラウザのcookiesをハッシュのように扱うことができます。cookiesの各要素は、それ自体が2つの要素 (valueとオプションのexpires日時) のハッシュになっています。たとえば以下のように、cookiesに20年後に期限切れになる記憶トークンに等しい値を保存することで、ユーザーのサインインを実装できます。

cookies[:remember_token] = { value:   remember_token,
                             expires: 20.years.from_now.utc }

(上のコードではRailsの便利なtimeヘルパーを使用しています。コラム 8.1を参照してください。)

上のように20年で期限切れになるcookies設定はよく使われるようになり、今ではRailsにも特殊なpermanentという専用のメソッドが追加されたほどです。このメソッドを使用すると、コードは以下のようにシンプルになります。

cookies.permanent[:remember_token] = remember_token

上のコードでは、permanentメソッドによって自動的に期限が20.years.from_nowに設定されます。

cookiesを設定後、移動先のページで以下のようなコードを使用してユーザーを取り出すことができます。

User.find_by(remember_token: remember_token)

もちろん、このcookies本物のハッシュではなく、実際にはcookiesに割り当てを行ったときにブラウザ上のテキストの断片を保存しているだけです。そうしたアプリケーションの実装の詳細を気にしなくてよい点に、Railsの美しさの一端が垣間見えます。

8.2.3現在のユーザー

後で使うために記憶トークンをcookiesに保存する方法の説明が終わりましたので、今度は移動先のページでユーザーを取り出す方法について学びましょう。sign_in関数のコードをもういちどよく見てみてください。

module SessionsHelper

  def sign_in(user)
    remember_token = User.new_remember_token
    cookies.permanent[:remember_token] = remember_token
    user.update_attribute(:remember_token, User.encrypt(remember_token))
    self.current_user = user
  end
end

現時点では、上のコードのうち、以下のコードだけが動作していません。

self.current_user = user

この行の目的は、コントローラからもビューからもアクセスできるcurrent_userを作成することです。これにより、以下のような構文を使用できるようになります。

<%= current_user.name %>

以下も使用できるようになります。

redirect_to current_user

リスト8.18のときに説明したのと同じ理由で、ここにもselfが必要です。selfがないと、Rubyは単にcurrent_userというローカル変数を作成してしまいます。

current_userのコードを書く上で、以下の行については注意が必要です。

self.current_user = user

上は「要素代入 (assignment)」であることに注意してください。このcurrent_user=は別途定義が必要です。リスト8.20に示したように、Rubyにはこのような要素代入関数を定義する特殊な文法があります。

リスト8.20 current_userへの要素代入を定義する。
app/helpers/sessions_helper.rb
module SessionsHelper

  def sign_in(user)
    .
    .
    .
  end

  def current_user=(user)
    @current_user = user
  end
end

上のコードで使用されている特殊な文法は混乱しやすいので注意してください。普通のプログラミング言語では、定義するメソッドの名前に等号を使用することはできませんが、Rubyではメソッド名末尾の等号には特殊な意味があり、上のコードはcurrent_userへの要素代入を扱うように設計されたcurrent_user=というメソッドを単に定義します。つまり、以下のコードは

self.current_user = ...

自動的に以下のコードに置き換えられます。

current_user=(...)

そして最終的にcurrent_user=というメソッドが呼び出されます。その引数は要素代入の右側にひとつ置かれます (ここではサインインするユーザー)。この一行メソッドは、単に@current_userインスタンス変数を設定し、後に使用するためにユーザーを効率よく保存します。

通常のRubyでは、もうひとつのメソッドとしてcurrent_userを定義することができます。リスト8.21に示したように、このメソッドは@current_userの値を返すようになっています。

リスト8.21 つい使ってみたくなるが実際には役に立たないcurrent_userの定義
module SessionsHelper

  def sign_in(user)
    .
    .
    .
  end

  def current_user=(user)
    @current_user = user
  end

  def current_user
    @current_user     # 役に立たない。この行は使用しないこと。
  end
end

もしかすると、これを使用して4.4.5attr_accessorを効率よく置き換えることができるのではないでしょうか5。それをしないのは、このコードを使用すると今までの苦労が台無しになってしまうからです。リスト8.21のコードを使用すると、ユーザーが別のページに移動した瞬間にユーザーのサインイン情報がきれいさっぱり消滅してしまいます。セッションは終了し、ユーザーは自動的にサインアウトしてしまいます。この問題を回避するには、リスト8.22に示したように、リスト8.19のコードで作成した記憶トークンに対応するユーザーを検索します。データベース上の記憶トークンは暗号化されているので、cookiesから取り出した記憶トークンは、データベース上の記憶トークンを検索する前に暗号化する必要がある点に注意してください。これを行うには、リスト8.18で定義したUser.encryptを使用します。

リスト8.22 remember_tokenを使用して現在のユーザーを検索する。
app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  def current_user=(user)
    @current_user = user
  end

  def current_user
    remember_token = User.encrypt(cookies[:remember_token])
    @current_user ||= User.find_by(remember_token: remember_token)
  end
end

リスト8.22では、Rubyでは定番の (しかし最初はまごつきやすい) ||= (“or equals”) 代入演算子を使用しています (コラム 8.2)。この代入演算子は、@current_userが未定義の場合にのみ、@current_userインスタンス変数に記憶トークンを設定します6

@current_user ||= User.find_by(remember_token: remember_token)

あるユーザーに対してcurrent_userが初めて呼び出される場合はfind_byメソッドを呼び出しますが、以後の呼び出しではデータベースにアクセスせずに@current_userを返します7。このコードは、ひとつのユーザーリクエストに対してcurrent_userが何度も使用される場合にのみ有用です。いずれの場合も、ユーザーがWebサイトにアクセスするとfind_byは最低1回は呼び出されます。

8.2.4レイアウトリンクを変更する

サインイン/サインアウトが動作するようになり、実用的なアプリケーションらしくなってきました。今度は、サインインの状態に合わせてレイアウト上のリンクが変わるようにしましょう。特に、図8.3のモックアップで示したように、ユーザーがサインインしているときとサインアウトしているときでリンクを変更し、すべてのユーザーを表示するリンクとユーザー設定用のリンクも追加し (第9章で完成する予定)、さらに現在のユーザーのプロファイルのリンクも追加します。これらを実装することで、リスト8.6のテストがパスするようになります。つまり、本章開始以来初めてテストスイート全体が緑色 (成功) になるということです。

サイトレイアウトのリンクを変更するには、埋め込みRubyの内側でif-else分岐構造を使用します。

<% if signed_in? %>
  # サインインしているユーザー用のリンク
<% else %>
  #サインインしていないユーザー用のリンク
<% end %>

この種のコードでは、signed_in? 論理値が必要になりますので、これから定義しましょう。

ユーザーがサインインしている状態は、セッションに現在のユーザーがいる (current_usernilでない) ことで表されます。これを表現するには否定の演算子が必要なので、! ("bang" と読みます) を使用します。リスト8.23に示すとおり、現在の文脈ではcurrent_usernilない場合にユーザーがサインインしています。

リスト8.23 signed_in?ヘルパーメソッド。
app/helpers/sessions_helper.rb
module SessionsHelper

  def sign_in(user)
    remember_token = User.new_remember_token
    cookies.permanent[:remember_token] = remember_token
    user.update_attribute(:remember_token, User.encrypt(remember_token))
    self.current_user = user
  end

  def signed_in?
    !current_user.nil?
  end
  .
  .
  .
end

signed_in?メソッドを手作りしてあるので、レイアウトのリンクはすぐに作成できます。なお、新しく作るリンクは4つですが、そのうち以下の2つのリンクは未実装です (第9章で完成の予定)。

<%= link_to "Users", '#' %>
<%= link_to "Settings", '#' %>

サインアウトのリンクには、リスト8.2で定義したサインアウトのパスを使用します。

<%= link_to "Sign out", signout_path, method: "delete" %>

(上のコードでは、サインアウトのリンクがハッシュ引数を渡していることに注目してください。このハッシュ引数は、HTTPのDELETEリクエストを使用して送信される必要があることを示しています8)。

<%= link_to "Profile", current_user %>

なお、上のコードは以下のように書くこともできます。

<%= link_to "Profile", user_path(current_user) %>

Railsでは、このユーザーへの直接リンクが許されるので、この場合current_useruser_path(current_user)に自動的に変換されます。

新しいリンクをレイアウトに追加するときに、Bootstrapの機能を使用してドロップダウンメニューを実現しましょう。詳細については「Bootstrapコンポーネント (英語)」を参照してください。完全なコードをリスト8.24に示します。このコードでは、Bootstrapのドロップダウンメニューに関連するCSSのidとクラスが与えられていることに注目してください。

リスト8.24 サインインしているユーザー用にリンクを変更する。
app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="navbar-inner">
    <div class="container">
      <%= link_to "sample app", root_path, id: "logo" %>
      <nav>
        <ul class="nav pull-right">
          <li><%= link_to "Home", root_path %></li>
          <li><%= link_to "Help", help_path %></li>
          <% if signed_in? %>
            <li><%= link_to "Users", '#' %></li>
            <li id="fat-menu" class="dropdown">
              <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                Account <b class="caret"></b>
              </a>
              <ul class="dropdown-menu">
                <li><%= link_to "Profile", current_user %></li>
                <li><%= link_to "Settings", '#' %></li>
                <li class="divider"></li>
                <li>
                  <%= link_to "Sign out", signout_path, method: "delete" %>
                </li>
              </ul>
            </li>
          <% else %>
            <li><%= link_to "Sign in", signin_path %></li>
          <% end %>
        </ul>
      </nav>
    </div>
  </div>
</header>

ドロップダウンメニューを実現するには、BootstrapのJavaScriptライブラリが必要です。このライブラリは、RailsのAsset Pipelineを使用してインクルードすることができます。そのためには、リスト8.25に示すようにアプリケーションのJavaScriptファイルを編集します。

リスト8.25 Bootstrap JavaScriptライブラリをapplication.jsに追加する。
app/assets/javascripts/application.js
//= require jquery
//= require jquery_ujs
//= require bootstrap
//= require turbolinks
//= require_tree .

上のコードは、Sprocketsライブラリを使用してBootstrap JavaScriptをインクルードします。5.1.2bootstrap-sass gemを導入してあるので、これでBootstrapが利用できるようになります。

リスト8.24のコードを使用することで、すべてのテストにパスするはずです。

$ bundle exec rspec spec/

以上の変更を行ったことで、図8.9に示したように、リスト8.24で定義したドロップダウンメニューがサインインしたユーザーに表示されるようになりました。

profile_with_signout_link_bootstrap
図8.9サインインしたユーザーにリンクとドロップダウンが表示されるようになった。(拡大)

この時点で、ブラウザで実際にサインインできることを確認し、続いてブラウザを閉じてから再度サンプルアプリケーションを表示するとサインインしたままになっていることを確認してください。その気になれば、ブラウザのcookiesをブラウザで調べて結果を直接確認することもできます (図8.10)。

cookie_in_browser
図8.10ローカルのブラウザで記憶トークンのcookiesを表示する。(拡大)

8.2.5ユーザー登録と同時にサインインする

認証機能の基本的な部分はできましたが、ユーザーが登録を行った後、そのユーザーがデフォルトではサインインしておらず、このままではユーザーが混乱する可能性があります。そこで、サインアウトの機能を実装する前にその部分をもう少し作り込みましょう。最初に、認証のテストを追加します (リスト8.26)。ここには、7.6の演習にあるリスト7.32でも使った “after saving the user” describeブロックが含まれています。このリストに対応する演習を行なっていなかった方は、この時点で追加する必要があります。

リスト8.26 新規ユーザー登録後にユーザーがサインインしたことをテストする。
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do
    .
    .
    .
    describe "with valid information" do
      .
      .
      .
      describe "after saving the user" do
        before { click_button submit }
        let(:user) { User.find_by(email: 'user@example.com') }

        it { should have_link('Sign out') }
        it { should have_title(user.name) }
        it { should have_selector('div.alert.alert-success', text: 'Welcome') }
      end
    end
  end
end

上のコードでは、ユーザー登録後にサインインしていることを確認するために、サインアウト用のリンクが表示されているかどうかをテストしています。

8.2sign_inメソッドを作成してあるので、ユーザーをデータベースに保存した直後にsign_in @userを追加するだけで、ユーザーが実際にサインインしてテストにパスするようになります (リスト8.27)。

リスト8.27 ユーザー登録後にサインアップする。
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      sign_in @user
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end
  .
  .
  .
end

8.2.6サインアウトする

8.1で説明したとおり、このアプリケーションで使用する認証モデルでは、ユーザーは明示的にサインアウトするまでサインインしたままになります。この節では、そのサインアウト機能を追加します。

Sessionsコントローラのアクションは、これまでもRESTful慣例に従ってサインインページにはnewを使用し、サインインの完了にはcreateを使用しました。今回も同様に慣例に従い、セッションの削除 (サインアウト) にはdestroyを使用します。このことをテストするには、[Sign out] リンクをクリックして、そこにサインイン用のリンクが再び現れるかどうかを確認します (リスト8.28)。

リスト8.28 ユーザーのサインアウトをテストする。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "signin" do
    .
    .
    .
    describe "with valid information" do
      .
      .
      .
      describe "followed by signout" do
        before { click_link "Sign out" }
        it { should have_link('Sign in') }
      end
    end
  end
end

サインインがsign_in関数に依存しているのと同様に、サインアウトでもsign_out関数を単に実行します (リスト8.29)。

リスト8.29 セッションを削除する (ユーザーのサインアウト)。
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  .
  .
  .
  def destroy
    sign_out
    redirect_to root_url
  end
end

他の認証用機能と同様に、sign_outもSessionsヘルパーモジュールの中に置きます。実装は極めてシンプルです。現在のユーザーをnilにし、deleteメソッドを使用して、cookiesにあるセッションの記憶トークンを削除します (リスト8.30)。(destroyアクション内ですぐにリダイレクトされるので、現在のユーザーをnilにする必要は必ずしもありません。ただし、リダイレクトなしでsign_outを使用したい状況が今後発生するかもしれないので、そのときに役立つでしょう。)

リスト8.30 Sessionsヘルパーモジュールのsign_outメソッド。
app/helpers/sessions_helper.rb
module SessionsHelper

  def sign_in(user)
    remember_token = User.new_remember_token
    cookies.permanent[:remember_token] = remember_token
    user.update_attribute(:remember_token, User.encrypt(remember_token))
    self.current_user = user
  end
  .
  .
  .
  def sign_out
    self.current_user = nil
    cookies.delete(:remember_token)
  end
end

これでユーザー登録/サインイン/サインアウトがすべて揃いました。テストスイートはパスするはずです。

$ bundle exec rspec spec/

本書のテストスイートは認証システムをほぼカバーしていますが、すべてをカバーしているわけではありません。この点をご了承ください。たとえば、[このアカウント設定を保存する] の cookies (remember me) が有効になっているか、その後も保持されているかどうかのテストは含まれていません。このテストは作成可能ですが、著者の経験上、cookiesの値を直接調べる方法はRailsの実装に左右されやすい傾向があり、次のバージョンのRailsではまた変わるかもしれません。その場合、アプリケーションコードは正常に動作してもテストが正常に動作しなくなります。このような理由で、本書のテストコードでは、コアとなるアプリケーションコードをテストする際に高度な機能 (ユーザーがサインインできること、ページ移動後もサインインしていること、サインアウトできること) に重点を置き、重要性の低い機能は必ずしも含んでいません。

8.3Cucumberの紹介 (オプション)

サンプルアプリケーションの認証システムの基礎部分が無事完成したので、この機会にCucumberを使用したサインインのテスト方法をご紹介します。Cucumberは振舞駆動開発用のツールとして有名で、Rubyコミュニティに多くの愛用者がいます。この節の内容は必須ではありませんので、スキップしても問題ありません。

Cucumberを使用すると、アプリケーションの振る舞いをテキストベースの「ストーリー」で定義することができます。多くのRailsプログラマーは、Cucumberは顧客と共同作業するときに便利であることを知っています。Cucumberのストーリーは、専門知識のない人でも読むことができ、ストーリーを顧客と共有したり、場合によっては顧客がストーリーを作成することすらできるためです。もちろん、テスティングのフレームワークが純粋なRubyでないという点は残念でもあり、著者にとってはテキストベースのストーリーはいささか冗長な面もあると思われます。それでもCucumberはRubyのテスティングツールキットとして確固たる地位を占めており、著者としては低レベルの実装を気にすることなく高度な振る舞いを記述できる点が特に気に入っています。

本書ではRSpecとCapybaraをテスティングのメインに据えているので、この節のCucumberに関する説明は完全ではなく、表面的で物足りないことでしょう。この節の目的はCucumberのおいしさ (間違いなくシャキシャキして汁気たっぷりです) を知ってもらうための、いわば試食です。もし気に入っていただけたら、テスティングツールに関する完結した書籍がいくつもあります。(おすすめは「The RSpec Book 」(David Chelimsky著)、「 Rails 3 in Action 」(Ryan Bigg、Yehuda Katz著)、 「The Cucumber Book」(Matt Wynne、Aslak Hellesøy著) です)。(訳注: 日本語で読めるCucumberの書籍としては「はじめる!Cucumber」(諸橋 恭介著) があります。)

8.3.1インストールと設定

Cucumberをインストールするには、最初にcucumber-rails gemとdatabase_cleanerというユーティリティgemをGemfile:testグループに追加します (リスト8.31)。

リスト8.31 cucumber-rails gemをGemfileに追加する。
.
.
.
group :test do
  .
  .
  .
  gem 'cucumber-rails', '1.4.0', :require => false
  gem 'database_cleaner', github: 'bmabey/database_cleaner'
end
.
.
.

次に、いつものように以下を実行します。

$ bundle install

アプリケーションでCucumberを使用するための設定を行うために、次は必要なサポート用ファイルとディレクトリを生成します。

$ rails generate cucumber:install

上のコマンドにより、Cucumber関連のファイルが置かれるfeatures/ディレクトリが作成されます。

8.3.2フィーチャーとステップ

Cucumberでは、Gherkin (キュウリ属の植物: ガーキン) と呼ばれるテキストベースの言語を使用して、アプリケーションに期待される振る舞いを記述します。Gherkinで書かれたテストは、ちゃんと書かれたRSpecの例と同じぐらい読みやすくできています。どちらもテキストベースであり、自然な英語に近くRubyコードよりも読みやすいためです。

ここでは、リスト8.5リスト8.6のサインインのテスト例のサブセットをCucumberで実装してみましょう。最初に、features/ディレクトリ内にsigning_in.featureというファイルを作成します。

Cucumberのフィーチャーファイルは、以下のようにその機能の簡単な説明から始まります。

Feature: Signing in

次に個別のシナリオを追加します。たとえば、サインイン失敗をテストするには、以下のようなシナリオを作成します。

  Scenario: Unsuccessful signin
    Given a user visits the signin page
    When he submits invalid signin information
    Then he should see an error message

同様に、サインイン成功をテストするために以下を使用できます。

  Scenario: Successful signin
    Given a user visits the signin page
      And the user has an account
    When the user submits valid signin information
    Then he should see his profile page
      And he should see a signout link

これらをまとめたものをリスト8.32に示します。

リスト8.32 ユーザーのサインインをテストするCucumberのフィーチャーファイル。
features/signing_in.feature
Feature: Signing in

  Scenario: Unsuccessful signin
    Given a user visits the signin page
    When he submits invalid signin information
    Then he should see an error message

  Scenario: Successful signin
    Given a user visits the signin page
      And the user has an account
    When the user submits valid signin information
    Then he should see his profile page
      And he should see a signout link

これらのフィーチャーを実行するには、cucumber実行ファイルを以下のように実行します。

$ bundle exec cucumber features/

上のCucumberのコマンドを、下のRSpecのコマンドと比較してみてください。

$ bundle exec rspec spec/

ここからおわかりだと思いますが、CucumberはRSpecと同様Rakeタスクから呼び出すこともできます。

$ bundle exec rake cucumber

(rake cucumber:okと書くこともできます)。

まだテキストを書いただけなので、当然ながらこのままではCucumberのシナリオはテストにパスしません。テストスイートが緑色 (成功) になるためには、テキストファイルをRubyコードにマップするステップファイルを作成します。ステップファイルはfeatures/step_definitionsディレクトリに置きます。ファイル名はここではauthentication_steps.rbとします。

Feature行とScenario行は説明のためのものですが、それ以外の行はRubyに対応付けられる必要があります。たとえば、フィーチャーファイルにある以下のコードは、

Given a user visits the signin page

以下のステップ定義によって扱われます。

Given /^a user visits the signin page$/ do
  visit signin_path
end

フィーチャーファイルの中ではGivenは単なる文字列ですが、ステップファイルの中ではGivenメソッドであり、正規表現とブロックを引数に取ります。この正規表現はシナリオの中の行とマッチし、次のブロックの内容は、そのステップを実装するために必要なRubyのコードです。この場合、“a user visits the signin page”という記述は以下のコードによって実装されます。

visit signin_path

上のコードはどこかで見たことがあると思ったら、それもそのはず、Capybaraです。CapybaraはデフォルトでCucumberのステップファイルに含まれます。次の2行もわかりやすいと思います。

When he submits invalid signin information
Then he should see an error message

上のフィーチャーファイルのコードは、ステップファイルでは以下のように扱われます。

When /^he submits invalid signin information$/ do
  click_button "Sign in"
end

Then /^he should see an error message$/ do
  expect(page).to have_selector('div.alert.alert-error')
end

最初のステップでもCapybaraが使用されていますが、その次のステップではCapybaraのpageオブジェクトとRSpecが併用されています。見てのとおり、RSpecとCapybaraで行えるテストは、すべてCucumberでも行えます。

残りのステップも同様に進められます。最終的なステップ定義ファイルをリスト8.33に示します。ステップを追加したら、以下を実行します。

$ bundle exec cucumber features/

テストにパスするまでこれを繰り返します。

リスト8.33 サインインフィーチャーがパスするための完全なステップ定義。
features/step_definitions/authentication_steps.rb
Given /^a user visits the signin page$/ do
  visit signin_path
end

When /^he submits invalid signin information$/ do
  click_button "Sign in"
end

Then /^he should see an error message$/ do
  expect(page).to have_selector('div.alert.alert-error')
end

Given /^the user has an account$/ do
  @user = User.create(name: "Example User", email: "user@example.com",
                      password: "foobar", password_confirmation: "foobar")
end

When /^the user submits valid signin information$/ do
  fill_in "Email",    with: @user.email
  fill_in "Password", with: @user.password
  click_button "Sign in"
end

Then /^he should see his profile page$/ do
  expect(page).to have_title(@user.name)
end

Then /^he should see a signout link$/ do
  expect(page).to have_link('Sign out', href: signout_path)
end

リスト8.33のコードにより、以下のCucumberのテストはパスするはずです。

$ bundle exec cucumber features/

8.3.3対比: RSpecのカスタムマッチャー

簡単なCucumberのシナリオをいくつか紹介したので、それらと同等のRSpecの例と比較してみましょう。まず、リスト8.32のCucumberフィーチャーと、それに対応するリスト8.33のステップ定義をもう一度見てみましょう。次に、以下のRSpecリクエストspec (結合テスト) を見てみましょう。

describe "Authentication" do

  subject { page }

  describe "signin" do
    before { visit signin_path }

    describe "with invalid information" do
      before { click_button "Sign in" }

      it { should have_title('Sign in') }
      it { should have_selector('div.alert.alert-error', text: 'Invalid') }
    end

    describe "with valid information" do
      let(:user) { FactoryGirl.create(:user) }
      before do
        fill_in "Email",    with: user.email.upcase
        fill_in "Password", with: user.password
        click_button "Sign in"
      end

      it { should have_title(user.name) }
      it { should have_link('Profile',     href: user_path(user)) }
      it { should have_link('Sign out',    href: signout_path) }
      it { should_not have_link('Sign in', href: signin_path) }
    end
  end
end

Cucumberと結合テストでそれぞれどのように実装されているかがおわかりいただけると思います。Cucumberのフィーチャーは非常に読みやすいのですが、その代わり実装されているコードから完全に切り離されていて、両刃の剣です。著者にとって、Cucumberは読みやすいが書くのは面倒、結合テストはプログラマーにとってはそれほど読みやすくない代わりに書くのははるかに楽、という印象です。

Cucumberではフィーチャーとステップが分離されていることにより、抽象度の高い記述が可能であるという効果があります。以下のフィーチャーは、エラーメッセージが表示されるはずであるということを記述しています。

Then he should see an error message

そして以下のステップファイルでは、このテストを実装しています。

Then /^he should see an error message$/ do
  expect(page).to have_selector('div.alert.alert-error', text: 'Invalid')
end

これが特に便利なのは、実装に依存するのが2番目の要素であるステップファイルだけである点です。そのため、たとえばエラーメッセージを表示するためのCSSを変更しても、フィーチャーファイルは変更不要です。

同様に、以下のようなコードを何度も書くのは多くの人にとって苦痛だと思います。

should have_selector('div.alert.alert-error', text: 'Invalid')

本当に行いたいのは、そのページでエラーメッセージが表示されることを示すことのはずです。しかも、上の記法は実装に密着しているので、実装が変更されたらそれらもすべて変更が必要になります。純粋なRSpecでは、カスタムマッチャーを使用してこの問題を解決することができます。カスタムマッチャーを使用すると、上のコードを以下のように簡潔に記述することができます。

should have_error_message('Invalid')

このマッチャーは、5.3.4full_titleテストヘルパーを置いたのと同じユーティリティファイル内で定義することができます。コード自体は以下のようになります。

RSpec::Matchers.define :have_error_message do |message|
  match do |page|
    expect(page).to have_selector('div.alert.alert-error', text: message)
  end
end

同様に、よく使われる操作をヘルパーメソッドとして定義することもできます。

def valid_signin(user)
  fill_in "Email",    with: user.email
  fill_in "Password", with: user.password
  click_button "Sign in"
end

作成したサポートコードをリスト8.34に示します (5.6の演習のリスト5.38リスト5.39の結果を含みます)。 この方法は、Cucumberのステップ定義よりも柔軟であることがわかってきました。特に、マッチャーやshouldヘルパーがvalid_signin(user)のように引数を自然に取ることができます。ステップ定義は正規表現マッチャーによって繰り返すことができますが、この手法は一般に厄介なものになりやすいという印象です。

リスト8.34 ヘルパーメソッドとカスタムRSpecマッチャーを追加する。
spec/support/utilities.rb
include ApplicationHelper

def valid_signin(user)
  fill_in "Email",    with: user.email
  fill_in "Password", with: user.password
  click_button "Sign in"
end

RSpec::Matchers.define :have_error_message do |message|
  match do |page|
    expect(page).to have_selector('div.alert.alert-error', text: message)
  end
end

リスト8.34を使用することで、以下のように書くことができます。

it { should have_error_message('Invalid') }

以下も同様です。

describe "with valid information" do
  let(:user) { FactoryGirl.create(:user) }
  before { valid_signin(user) }
  .
  .
  .

テストとサイトの実装を結びつける方法の例は他にも多数あります。現在のテストスイート全般にわたって、カスタムマッチャーとカスタムメソッドを作成してテストと実装の詳細を切り離す作業については、演習に回すことにします (8.5)。

8.4最後に

本章では多くの分野をカバーし、約束どおり、かつて未成熟だったアプリケーションを、ユーザー登録とログインをフル装備したWebサイトに変身させました。認証機能を完成させるためには、サインインの状態とユーザーidに基いてページのアクセスに制限を与える必要もあります。ページごとのアクセス制限機能の実装については、ユーザー情報を編集する機能と、ユーザーをシステムから削除できる権限を管理者に与える機能を実装するときに同時に行う予定です。このアクセス制限は、第9章の主な目標でもあります。

次の章に進む前に、変更をmasterブランチにマージしておきましょう。

$ git add .
$ git commit -m "Finish sign in"
$ git checkout master
$ git merge sign-in-out

次にリモートのGitHubリポジトリとHerokuの本番サーバーにプッシュします。

$ git push
$ git push heroku
$ heroku run rake db:migrate

8.5演習

  1. form_forの代わりにform_tagを使用して、サインインフォームをリファクタリングしてください。テストスイートが以前と同様にパスすることも確認してください。ヒント: RailsCastの「Rails 3.1における認証 (英語)」を参照してください。特に、paramsハッシュの構造の変更に注目してください。
  2. 8.3.3の例に従い、ユーザー用リクエストspecと認証リクエストspec (spec/requestsディレクトリの下にあるファイル) を見直し、spec/support/utilities.rbでユーティリティ関数を定義して、テストを実装から切り離してください。課外活動: サポートコードを独立したファイルとモジュールに再編成し、specヘルパーファイルでそれらのモジュールを適切にインクルードしてすべて動作するようにしてください。
  1. 他に、一定時間が経過するとセッションを期限切れにするモデルもあります。このモデルは、インターネットバンキングや金融取引口座などの重要な情報を扱うWebサイトに向いています。
  2. 画像はhttps://www.flickr.com/photos/hermanusbackpackers/3343254977/からの引用です。
  3. このメソッドは、RailsCastの「remember me」の記事を元に選びました。
  4. Active Recordでサポートされるコールバックの種類の詳細については、Railsガイドの「Active Record コールバック」を参照してください。
  5. 実は、この2つは完全に同等です。attr_accessorは、単にゲッターメソッドやセッターメソッドを自動的に作成する便利な方法でしかありません。
  6. 通常、これは初期値がnilである変数への代入を意味しますが、false値も||=演算子によって上書きされることに注意してください。
  7. これは、コラム 6.3で説明したメモ化 (memoization) の例になっています。
  8. Webブラウザは実際にはDELETEリクエストを発行できないので、RailsではJavaScriptを使用してこのリクエストを偽造します。
Railsチュートリアルは,Ruby/Rails のアジャイル開発を得意とする YassLab によって運営・保守されております.
継続的に良いコンテンツを提供する為に,電子書籍スクリーンキャストのご購入を検討して頂けると幸いです m(_ _)m
スポンサーシップや商用利用などに関するご相談がありましたら,お問い合わせページよりお気軽にご連絡ください :)