LoginSignup
0
0

More than 3 years have passed since last update.

Railsチュートリアル 第11章 アカウントの有効化 - アカウントを有効化する

Posted at

これから行っていくもの

  • AccountActivationsコントローラーのeditアクションの実装
  • 当該アクションに対するテストの実装
  • AccountActivationsコントローラーに関係するリファクタリング
    • AccountActivationsコントローラーからUserモデルにコードを移していく

authenticated?メソッドの抽象化

前提

アカウント有効化リンクにおけるparamsハッシュ

http://www.example.com/account_activations/C477rjyLBojP4v5nfKcoqQ/edit?email=foo%40example.com

例えば上述のようなURLでAccountActivations#editアクションを呼び出した場合、アカウントの有効化に必要な属性は、paramsハッシュを使って以下のように参照できます。

  • 有効化トークンは、params[:id]として参照できる
  • メールアドレスは、params[:email]として参照できる

既存のauthenticated?メソッド

今回製作中のサンプルアプリケーションには、すでに「与えられたトークンとRDBに記録されたダイジェストの両者を照合し、合致すれば認証する」という実装は存在します。User#authenticated?というメソッドがそれですね。

User#authenticated?
def authenticated?(remember_token)
  return false if remember_digest.nil?
  BCrypt::Password.new(remember_digest).is_password?(remember_token)
end

なお、User#authenticated?の実装については、大枠はそのままながら引数のとり方を変更していきます。

アカウント有効化リンクからユーザーを検索・認証する仕組み

上記前提を踏まえ、「アカウント有効化リンクからユーザーを検索・認証する仕組み」は以下のように実装していくことになります。

user = User.find_by(email: params[:email])
if user && user.authenticated?(:activation, params[:id])

User#authenticated?の実装の変更

remember_digestはUserモデルの属性です。Userモデル内においては、以下のように使うことができます。

self.remember_digest

では、上記remember_digestremenberという部分を可変にし、「状況に応じて呼び出すメソッドを変える」という実装を実現するためにはどうすればいいか。そこがUser#authenticated?の実装の変更ポイントとなります。すなわち、以下のような呼び出しができるようにしたい、ということです。

self.FOOBAR_digest

メタプログラミング

「プログラムそのものをプログラムによって作成する」というプログラミング手法のことです。「Rubyの言語体系に含まれるメタプログラミング手法は、Smalltalkの影響1が強いものである」とはよく言及されますね。とりわけRailsにおいては、メタプログラミング手法の多用により、「素のRubyとは別物」と言われるほどの言語体系が構築されるに至っています。

Railsチュートリアルの第11章においては、method_missing2までは言及されておらず、sendについてのみ言及されています。

sendメソッドの動作

# rails console --sandbox

>> a = [1, 2, 3]
>> a.length
=> 3
>> a.send(:length)
=> 3
>> a.send("length")
=> 3

上記Railsコンソールの例では、「sendメソッドにシンボル:lengthや文字列"length"を渡した結果は、lengthメソッドを直接実行した結果と同じになる」という点が重要です。

# rails console --sandbox

>> user = User.first

>> user.send(:activation_digest)
=> "$2a$10$Q.bVywQrrgEJC6Mg0IhdXONY5M/0jQYm4/ZEBwhJfxcag7VBBx6S6"
>> user.send("activation_digest")
=> "$2a$10$Q.bVywQrrgEJC6Mg0IhdXONY5M/0jQYm4/ZEBwhJfxcag7VBBx6S6"

>> attribute = :activation
=> :activation
>> user.send("#{attribute}_digest")
=> "$2a$10$Q.bVywQrrgEJC6Mg0IhdXONY5M/0jQYm4/ZEBwhJfxcag7VBBx6S6"

続いては、「sendメソッドを用いて、RDB上に存在する最初のユーザーのactivation_digest属性にアクセスする」という例です。activation_digestメソッドも、lengthメソッドと同様、「メソッド自身の名前を、sendの引数にシンボルや文字列として渡すことにより実行する」ことが可能です。

特に重要なのは、最後の「式展開により、呼び出すメソッドの名前の一部を変数としている」という実行例です。

「式展開により、呼び出すメソッドの名前の一部を変数とする」その動作の解説

まず、:activationというシンボル3を値とする変数attributeを定義します。

attribute = :activation

その上で、文字列の展開により引数を組み立てて…

"#{attribute}_digest"

sendメソッドに渡します。

send("#{attribute}_digest")
余談 - sendに類似するメソッドとか概念とか

そういえば、Excelのワークシート関数であるINDIRECTも、Rubyのsendと似たような使い方をしますよね。

sendに類似するメソッドとして、evalというメソッドも存在します。sendが「メソッド名」を渡して用いるのに対し、evalは「Rubyコードそのもの」を渡して用いるメソッドです。

実際にUser#authenticated?の実装を変更する

ここまでのポイントを踏まえた上で、実際にUser#authenticated?の実装内容に手を加えていきましょう。

User#authenticated?の新しい実装
def authenticated?(attribute, token)
  digest = send("#{attribute}_token")
  return false if digest.nil?
  BCrypt::Password.new(digest).is_password?(token)
end
app/models/user.rb
  class User < ApplicationRecord
  ...略

-   def authenticated?(remember_token)
+   def authenticated?(attribute, token)
+     digest = send("#{attribute}_digest")
-     return false if remember_digest.nil?
+     return false if digest.nil?
-     BCrypt::Password.new(remember_digest).is_password?(remember_token)
+      BCrypt::Password.new(digest).is_password?(token)
    end

    ...略
  end

既存のauthenticated?メソッド使用箇所の実装を変更する

現状ではテストが成功しない

現状では、アプリケーション全体に対するテストは成功しません。

# rails test
Running via Spring preloader in process 13778
Started with run options --seed 51138

ERROR["test_current_user_returns_nil_when_remember_digest_is_wrong", SessionsHelperTest, 3.314027799991891]
 test_current_user_returns_nil_when_remember_digest_is_wrong#SessionsHelperTest (3.31s)
ArgumentError:         ArgumentError: wrong number of arguments (given 1, expected 2)
            app/models/user.rb:31:in `authenticated?'
            app/helpers/sessions_helper.rb:26:in `current_user'
            test/helpers/sessions_helper_test.rb:17:in `block in <class:SessionsHelperTest>'

ERROR["test_current_user_returns_right_user_when_session_is_nil", SessionsHelperTest, 3.3577567999600433]
 test_current_user_returns_right_user_when_session_is_nil#SessionsHelperTest (3.36s)
ArgumentError:         ArgumentError: wrong number of arguments (given 1, expected 2)
            app/models/user.rb:31:in `authenticated?'
            app/helpers/sessions_helper.rb:26:in `current_user'
            test/helpers/sessions_helper_test.rb:11:in `block in <class:SessionsHelperTest>'

ERROR["test_authenticated?_should_return_false_for_a_new_user_with_nil_digest", UserTest, 5.586914699990302]
 test_authenticated?_should_return_false_for_a_new_user_with_nil_digest#UserTest (5.59s)
ArgumentError:         ArgumentError: wrong number of arguments (given 1, expected 2)
            app/models/user.rb:31:in `authenticated?'
            test/models/user_test.rb:75:in `block in <class:UserTest>'

  44/44: [=================================] 100% Time: 00:00:06, Time: 00:00:06

Finished in 6.20436s
44 tests, 185 assertions, 0 failures, 3 errors, 0 skips

「ArgumentError: wrong number of arguments (given 1, expected 2)」というメッセージが見受けられますね。これは、「authenticated?メソッドに渡す引数の数が不足している」という趣旨のメッセージです。

まずは、3つのエラー共通で報告されているapp/models/user.rb:31を修正していきましょう。

修正箇所1 - SessionsHelper#current_userメソッド

まずはapp/helpers/sessions_helper.rbの31行目を修正していきましょう。私の環境では、SessionsHelper#current_userに問題の行があります。

SessionsHelper#current_user
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
-     if user && user.authenticated?(cookies[:remember_token])
+     if user && user.authenticated?(:remember , cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

まだテストは成功しない

# rails test
Running via Spring preloader in process 13791
Started with run options --seed 46675

ERROR["test_authenticated?_should_return_false_for_a_new_user_with_nil_digest", UserTest, 3.5224237999645993]
 test_authenticated?_should_return_false_for_a_new_user_with_nil_digest#UserTest (3.53s)
ArgumentError:         ArgumentError: wrong number of arguments (given 1, expected 2)
            app/models/user.rb:31:in `authenticated?'
            test/models/user_test.rb:75:in `block in <class:UserTest>'

 FAIL["test_current_user_returns_right_user_when_session_is_nil", SessionsHelperTest, 4.250324499967974]
 test_current_user_returns_right_user_when_session_is_nil#SessionsHelperTest (4.25s)
        --- expected
        +++ actual
        @@ -1 +1 @@
        -#<User id: 959740715, name: "Reimu Hakurei", email: "rhakurei@example.com", created_at: "2019-12-06 09:01:01", updated_at: "2019-12-06 09:01:05", password_digest: "$2a$04$HVRWhbBA8amg3yoSSEKV9.vf6jg/T01/QCKfSI.w6ML...", remember_digest: "$2a$04$W7PA7NZAg5QbFQDgfqZQo.bmSUdr6OBKJ01JZKkI0gj...", admin: true, activation_digest: nil, activated: true, activated_at: nil>
        +nil
        test/helpers/sessions_helper_test.rb:11:in `block in <class:SessionsHelperTest>'

  44/44: [=================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.53206s
44 tests, 187 assertions, 1 failures, 1 errors, 0 skips

「ArgumentError: wrong number of arguments (given 1, expected 2)」というメッセージがまだ見受けられます。今度はtest/models/user_test.rb:75とあります。修正していきましょう。

修正箇所2 - UserTestの「authenticated? should return false for a new user with nil digest」テスト

  test "authenticated? should return false for a new user with nil digest" do
-   assert_not @user.authenticated?('')
+   assert_not @user.authenticated?(:remember, '')
  end

まだ何かがおかしい

これで改めてテストを実行してみます。しかし、まだ成功しません。

# rails test
Running via Spring preloader in process 13807
Started with run options --seed 22372

 FAIL["test_current_user_returns_right_user_when_session_is_nil", SessionsHelperTest, 2.9952282999875024]
 test_current_user_returns_right_user_when_session_is_nil#SessionsHelperTest (3.00s)
        --- expected
        +++ actual
        @@ -1 +1 @@
        -#<User id: 959740715, name: "Reimu Hakurei", email: "rhakurei@example.com", created_at: "2019-12-06 09:11:02", updated_at: "2019-12-06 09:11:05", password_digest: "$2a$04$lRWR5q0KpF3UiW2r.hyD4e19FtV4Na6gh7iwkvdiely...", remember_digest: "$2a$04$26R0Y3nwF/Wh68N94ShIze7j6dLAT03Hva5oDt4PhP5...", admin: true, activation_digest: nil, activated: true, activated_at: nil>
        +nil
        test/helpers/sessions_helper_test.rb:11:in `block in <class:SessionsHelperTest>'

  44/44: [=================================] 100% Time: 00:00:05, Time: 00:00:05

Finished in 5.43956s
44 tests, 188 assertions, 1 failures, 0 errors, 0 skips

Railsチュートリアル本文に言及されていない何かがおかしい、ということですね。

修正箇所3 - authenticated?に関する想定外のテスト失敗と、その解決

長くなりましたので、別記事で解説します。

これでようやくテストが成功

# rails test
Running via Spring preloader in process 13904
Started with run options --seed 21760

  44/44: [=================================] 100% Time: 00:00:06, Time: 00:00:06

Finished in 6.01140s
44 tests, 189 assertions, 0 failures, 0 errors, 0 skips

ようやくテストが成功しました。正直ここまで長かったです。

演習 - authenticated?メソッドの抽象化

長くなりましたので、別記事で解説します。

editアクションで有効化

ユーザーが有効化対象であることの確認

以下のif文が核となります。

if user && !user.activated? && user.authenticated?(:activation, params[:id])

!user.activated?なる文の必要性

「既に有効化済みのユーザーに対し、再度有効化処理を実行することはしない」という意味の文です。このような文が必要となる理由は以下です。

  • 「有効化が行われたら、その時点で当該ユーザーはログイン状態となる」という実装を行う
    • ユーザー体験を考えると、ユーザーの有効化とログインはセットで行われるのが好ましいため
  • 上記のような実装の場合、何も対策をしていないと、「有効化リンクが盗み出されたら、そのリンクを使って自由にログインできてしまう」という脆弱性が発生する
  • 以上の脆弱性への対策として、「既に有効化済みのユーザーに対し、再度有効化処理を実行することはしない」という実装を行う

その他の部分

以下の事柄について確認を行っています。

  • userが存在するユーザーであること
  • 与えられたトークンが正しい有効化トークンであること

今回定義するアカウント有効化リンクにおいては、「有効化トークンは、params[:id]として参照できる」のでしたよね。

アカウント有効化と、付随する処理の実体

コードは以下のようになります。

user.update_attribute(:activated,    true)
user.update_attribute(:activated_at, Time.zone.now)
log_in user
flash[:success] = "Account activated!"
redirect_to user

処理の流れは以下のようになります。

  1. 有効化されるユーザーの:activated属性の値にtrueを格納する
  2. 有効化されるユーザーの:actiated_at属性の値に現在時刻を格納する
  3. 有効化されたユーザーとしてログイン処理を行う
  4. 正常に処理が行われた旨のフラッシュメッセージを定義する
  5. 有効化されたユーザーのプロフィールページにリダイレクトする

ポイントは以下です。

  • RDBへの変更の反映は、update_attributeメソッドによって行っている
    • バリデーションをバイパスする必要があるため
  • 有効化されたユーザーとしてログインする処理を行っている
  • 有効化されたユーザーのプロフィールページにリダイレクトされている

有効化URLによりユーザーを有効化する

ここまでの実装が完了すれば、メールで送信されたURLを使ってユーザーを有効化することができるようになります。

メール本文
Hi Foo Bar Baz,

Welcome to the Sample App! Click on the link below to activate your account:

https://localhost:3000/account_activations/8I_nvDPPSA3TIqvNT4n9qA/edit?email=foobar%2Bfoobar%40example.org

ポート番号の変更

私の環境では、Docker仮想環境側のTCP3000番ポートをローカル側のTCP8080番ポートと紐付けているため、localhost:3000という記述をlocalhost:8080に変更する必要があります。

スキームをhttps://からhttp://に変更する

また、スキームがhttps://だと以下のエラーで有効化URLにアクセスできませんでした。

HTTP parse error, malformed request (): #<Puma::HttpParserError: Invalid HTTP format, parsing fails.>

スキームがhttp://だとアクセスできます。

最終的な有効化URL

最終的には、ローカル側でアクセスする有効化URLは以下のようになります。

- https://localhost:3000/account_activations/8I_nvDPPSA3TIqvNT4n9qA/edit?email=foobar%2Bfoobar%40example.org
+ http://localhost:8080/account_activations/8I_nvDPPSA3TIqvNT4n9qA/edit?email=foobar%2Bfoobar%40example.org

有効化が成功する

有効化が成功すると、Webブラウザで以下のような画面が返ってきます。

スクリーンショット 2019-12-08 14.28.56.png

上述ユーザーのIDが102であることを踏まえて、Railsコンソールでactivated?activated_atの状態を確認してみます。

# rails console --sandbox

>> user = User.find(102)
>> user.activated?
=> true
>> user.activated_at
=> Sun, 08 Dec 2019 05:20:45 UTC +00:00

正常に有効化処理が行われたようですね。

同じ有効化URLを再度使うことはできない

同じ有効化URLで再度アクセスを試みると、以下のような画面が返ってきます。

スクリーンショット 2019-12-08 14.34.42.png

「Invalid activation link」のフラッシュメッセージと、/ へのリダイレクト。こちらも想定どおりの挙動ですね。

「ユーザーが有効化されていなければログインできない」という動作の実装

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])
-       log_in @user
-       params[:session][:remember_me] == '1' ? remember(@user) : forget(@user)
-       redirect_back_or @user
+       if user.activated?
+         log_in @user
+         params[:session][:remember_me] == '1' ? remember(@user) : forget(@user)
+         redirect_back_or @user
+       else
+         message  = "Account not activated."
+         message += "Check your email for activation link."
+         flash[:warning] = message
+         redirect_to root_url
+       end
      else
        ...略
      end
    end

    ...略
  end

ログインしようとしたユーザーが有効化されていない場合は、「送付されたメールの確認を促すフラッシュメッセージを表示するようにした上で、/ にリダイレクト」という処理が行われます。

演習 - editアクションで有効化

1. コンソールから、11.2.4で生成したメールに含まれているURLを調べてみてください。URL内のどこに有効化トークンが含まれているでしょうか?

https://localhost:3000/account_activations/8I_nvDPPSA3TIqvNT4n9qA/edit?email=foobar%2Bfoobar%40example.org

上記URLを前提とすると、 /edit の前にある以下の文字列が有効化トークンとなります。

8I_nvDPPSA3TIqvNT4n9qA

Railsアプリケーション側では、params[:id]として参照できます。

2. 先ほど見つけたURLをブラウザに貼り付けて、そのユーザーの認証に成功し、有効化できることを確認してみましょう。また、有効化ステータスがtrueになっていることをコンソールから確認してみてください。

上述「有効化URLによりユーザーを有効化する」の通りです。

  • ポート番号が違うという問題に対する解決
  • Docker環境において、https://スキームで有効化URLを扱えるようにする方法

以上の課題は今後に持ち越しでしょうか。

有効化のテストとリファクタリング

有効化のテスト

テストコードの全体像

test/integration/users_signup_test.rbの全体像は以下のようになります。

test/integration/users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest

  def setup
    ActionMailer::Base.deliveries.clear
  end

  test "invalid signup information" do
    get signup_path
    assert_select 'form[action="/signup"]'
    assert_no_difference 'User.count' do
      post signup_path, params: { user: { name: "",
                                        email: "user@invalid",
                                        password:              "foo",
                                        password_confirmation: "bar" } }
    end
    assert_template 'users/new'
    assert_select 'div#error_explanation'
    assert_select 'div.field_with_errors'
  end

  test "valid signup information with account activation" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name: "Example User",
                                         email: "user@example.com",
                                         password: "password",
                                         password_confirmation: "password"} }
    end
    assert_equal 1, ActionMailer::Base.deliveries.size
    user = assigns(:user)
    assert_not user.activated?
    # 有効化していない状態でログインしてみる
    log_in_as(user)
    assert_not is_logged_in?
    # 有効化トークンが不正な場合
    get edit_account_activation_path("invalid token", email: user.email)
    assert_not is_logged_in?
    # トークンは正しいがメールアドレスが無効な場合
    get edit_account_activation_path(user.activation_token, email: 'wrong')
    assert_not is_logged_in?
    # 有効化トークンが正しい場合
    get edit_account_activation_path(user.activation_token, email: user.email)
    assert user.reload.activated?
    follow_redirect!
    assert_template 'users/show'
    assert_not flash.empty?
    assert is_logged_in?
  end
end

配信されたメッセージがきっかり1通であることを確認する

以下のコードにより、「メイラーによって配信されたメッセージが1通であること」を確認できます。

assert_equal 1, ActionMailer::Base.deliveries.size

但し、ActionMailer::Base.delivariesは変数であり、メイラーを使う他のテストで上書きされる可能性があることに注意しなければなりません。このテストが期待通りに行われるためには、setupメソッドでdelivariesを初期化しておく必要があります。

UsersSignupTest#setuop
def setup
  ActionMailer::Base.deriveries.clear
end

テストが成功することの確認

コードにtypo等がなければ、この時点でテストは成功するはずです。

# rails test
Running via Spring preloader in process 14065
Started with run options --seed 9265

  44/44: [=================================] 100% Time: 00:00:10, Time: 00:00:10

Finished in 10.72402s
44 tests, 195 assertions, 0 failures, 0 errors, 0 skips

私がやってしまったtypo

私は多数のtypoをやってしまったため、テストが成功するまでに数回エラーを出してしまいました。私がやってしまったtypoの例は以下です。

- if user.activated?
+ if @user.activated?
- deriveries
+ deliveries
- delivaries
+ deliveries
- user = assigns(:user)
+ user = assigns(@user)
- assert is logged_in?
+ assert is_logged_in?

皆さんもtypoには気をつけましょう。

リファクタリング - Userモデルにユーザー有効化メソッドを追加する

ここまでのテストが実装できれば、「ユーザー操作の一部をコントローラーからモデルに移動する」というリファクタリングが可能になります。具体的には、以下の処理をコントローラーからモデルに移していくことになります。

  • ユーザーの有効化属性の更新
  • 有効化メールの送信

ユーザーの有効化属性の更新

ユーザーの有効化属性を更新するactivateメソッドの実装は、以下のようになります。

User#activate
def activate
  update_attribute(:activated,    true)
  update_attribute(:activated_at, Time.zone.now)
end

update_attributeの呼び出しに際し、user.を頭につけなくなった」というのは大きなポイントです。user.がなくなるに至る経緯の説明は以下です。

  • Userモデルにuserという変数は存在しない
    • この点AccountActivationsコントローラーとは異なる
  • AccountActivationsコントローラーにおけるuser.に相当するのは、Userモデルにおいてはself.である
  • セッターメソッド4でない場合、レシーバーとして用いるself.は省略できる
    • update_attributeはセッターメソッドではない

有効化メールの送信

有効化メールを送信するsend_activation_emailメソッドの実装は、以下のようになります。

User#send_activation_email
def send_activation_email
  UserMailer.account_activation(self).deliver_now
end

こちらも「@userselfに変更されている」というのがポイントです。実装箇所がUsersコントローラーからUserモデルに変わったことにより、このような変更が必要となりました。

「実装箇所が変更されたことに伴う変数名の変更」について、Railsチュートリアル本文には、以下のような記述があります。

どんなに簡単なリファクタリングであっても、この手の変更はつい忘れてしまうものです。テストをきちんと書いておけば、この種の見落としを検出できます。

Userモデル全体の変更

app/models/user.rbに、activateメソッドとsend_activation_emailメソッドを追加していきます。

app/models/user.rb
  class User < ApplicationRecord
    ...略
+
+   # アカウントを有効にする
+   def activate
+     update_attribute(:activated,    true)
+     update_attribute(:activated_at, Time.zone.now)
+   end
+
+   # 有効化用のメールを送信する
+   def send_activation_email
+     UserMailer.account_activation(self).deliver_now
+   end

    private

      ...略
  end

Usersコントローラーのcreateアクションにおける、有効化メール送信処理の実装変更

有効化メールを送信する処理が行われるのは、Usersコントローラーのcreateアクション内です。Userモデルにsend_activation_emailメソッドを実装したので、Usersコントローラーのcreateアクションでも当該メソッドを呼び出すように実装を変更していきます。

UsersController#create
  def create
    @user = User.new(user_params)
    if @user.save
-     UserMailer.account_activation(@user).deliver_now
+     @user.send_activation_email
      flash[:info] = "Please check your email to activate your account."
      redirect_to root_url
    else
      render 'new'
    end
  end

AccoutActivationsコントローラーのeditアクションにおける、ユーザーの有効化処理の実装変更

ユーザーの有効化処理が行われるのは、AccountActivationsコントローラーののeditアクション内です。Userモデルにactivateメソッドを実装したので、AccoutActivationsコントローラーのeditアクションでも当該メソッドを呼び出すように実装を変更していきます。

AccountActivationsController#edit
  def edit
    user = User.find_by(email: params[:email])
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
-     user.update_attribute(:activated,    true)
-     user.update_attribute(:activated_at, Time.zone.now)
+     user.activate
      log_in user
      flash[:success] = "Account activated!"
      redirect_to user
    else
      flash[:danger] = "Invalid activation link"
      redirect_to root_url
    end
  end

テストが成功することを確認する

テストの成功を確認するところまでがリファクタリングです。実装の変更が完了したならば、すぐにテストに取り掛かりましょう。

# rails test
Running via Spring preloader in process 14117
Started with run options --seed 49116

  44/44: [=================================] 100% Time: 00:00:09, Time: 00:00:09

Finished in 9.66480s
44 tests, 195 assertions, 0 failures, 0 errors, 0 skips

テストは無事成功しました。

演習 - 有効化のテストとリファクタリング

1.1. リスト 11.39に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう (これでデータベースへの問い合わせが1回で済むようになります)。

リスト 11.35にあるactivateメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。

User#activate
  def activate
-   update_attribute(:activated,    true)
-   update_attribute(:activated_at, Time.zone.now)
+   update_columns(activated: true, activated_at: Time.zone.now)
  end

update_attributeと同様、update_columnsにおいてもバリデーションは行われません。この点はsaveupdate_attributesとは異なります。

ActiveRecordにおけるattributeの更新方法に関しては、@tyamagu2さんによる以下の記事が参考になります(ただし2016年時点の記事であることに留意)。

1.2. また、変更後にテストを実行し、greenになることも確認してください。

# rails test
Running via Spring preloader in process 14130
Started with run options --seed 41078

  44/44: [=================================] 100% Time: 00:00:07, Time: 00:00:07

Finished in 7.69770s
44 tests, 195 assertions, 0 failures, 0 errors, 0 skips

無事テストは成功しました。

2. リスト 11.40のテンプレートを使って、後述引用の動作を変更してみましょう。なお、ここで使っているActive Recordのwhereメソッドについては、13.3.3でもう少し詳しく説明します。

現在は、/usersのユーザーindexページを開くとすべてのユーザーが表示され、/users/:idのようにIDを指定すると個別のユーザーを表示できます。しかし考えてみれば、有効でないユーザーは表示する意味がありません。

3. ここまでの演習課題で変更したコードをテストするために、/users と /users/:id の両方に対する統合テストを作成してみましょう。

長くなりましたので、別記事で解説します。


  1. RegionalRubyKaigi レポート (13) 札幌 Ruby 会議 02の、項目「知らないと損をする Smalltalk」あたりが、「Smalltalkの影響」についての概説的な言及となっています。 

  2. 定義されていないメソッドの呼び出しに対する動作を定義するメソッドです。 

  3. attributeの値として、文字列'activation'を与えても、最終的な動作は変わりません。但し、「実行効率・使用メモリ量における優位性」等の理由により、Rubyにおける当該用法ではシンボルを使うことが一般的です。 

  4. 現在開発中のサンプルアプリケーションで用いているセッターメソッドの例としては、User#remember_tokenUser#activation_tokenがあります。 

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0