Help us understand the problem. What is going on with this article?

Railsチュートリアル 第11章 リスト11.26のテストでテスト結果がRedになるはずのところでGreenになってしまった。

過去の2つの記事との脈絡が無いのが申し訳ないのですが、現在Railsチュートリアルの2周目を行っていて、あまりにもQiitaへの投稿をこまめにやっていなかったのが自分でも気になり、練習がてら自分がハマってしまったことについて投稿する練習をしてみようと思い投稿しました。
内容的にあまりにも初歩的な内容のため誰かの参考になるような記事では無いと思います。

問題

リスト11.26からリスト11.30までテストの結果がRailsチュートリアルが意図するものと違う結果になってしまっていた。
リスト11.26
https://railstutorial.jp/chapters/account_activation?version=5.1#code-generalized_authenticated_p

本来このリスト11.26ではRailsチュートリアルの説明通り引数の数が合わないことが原因となるArgumentErrorが出るはずであったが、何故かErrorは出ずにGreenになってしまった。

リスト 11.26: 抽象化されたauthenticated?メソッド red

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # トークンがダイジェストと一致したらtrueを返す
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end
  .
  .
  .
end

リスト 11.26のキャプションに記したとおり、この時点ではテストスイートは redになります。

リスト 11.27: red

$ rails t

ここでRedになるはずだったのだが、
何故か自分の実際の環境ではGreenとなってしまった。

$ rails t
Running via Spring preloader in process 84886
Started with run options --seed 28770

  42/42: [=======================================================================================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.68394s
42 tests, 172 assertions, 0 failures, 0 errors, 0 skips

エラーであればエラーメッセージや結果に基づいてググるなりなんなりして解決出来ると考えたが、エラーではなく何も問題が無いと言われてしまうと今の自分の力では原因の特定は無理だと考えた。その後仕方なく進めて11.28などの作業をしてみた段階で新たなエラーが出たりしたらまたその時考えようと思い、11.28や11.29のリストの変更作業を行ってみた。その結果、リスト11.29ではGreenになるはずだったものが

Pry#input_array is deprecated. Use Pry#input_ring instead
19:34:42 - INFO - Running: test/models/user_test.rb
Running via Spring preloader in process 4899
Started with run options --seed 25892

ERROR["test_authenticated?_should_return_false_for_a_user_with_nil_digest", UserTest, 0.628540000000612]
 test_authenticated?_should_return_false_for_a_user_with_nil_digest#UserTest (0.63s)
ArgumentError:         ArgumentError: wrong number of arguments (given 2, expected 1)
            app/models/user.rb:39:in `authenticated?'
            test/models/user_test.rb:77:in `block in <class:UserTest>'

  12/12: [=========================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.71992s
12 tests, 19 assertions, 0 failures, 1 errors, 0 skips

となってしまった。
該当箇所となる

           app/models/user.rb:39:in `authenticated?'
            test/models/user_test.rb:77:in `block in <class:UserTest>'

の部分をいくらコードを見てみてもこの時は何がおかしいのか全く分からず途方に暮れてしまった。リストの内容でググってみても特にここでハマってしまったり自分と似たようなミスをしている人は居ないようで解決の手段が見当たらなかった。

原因

結局今の自力では原因を追求出来なかったのでRailsチュートリアルの11章終了時点でのリポジトリを見せてもらってそことの比較で自分のコードの悪い部分を見つけようと考えた。

またteratailに投稿があった、この内容も自分のコードのどこがおかしいかを考えるいいきっかけになった。
https://teratail.com/questions/214041
この質問とは結果的には自分の環境で起きていた問題とは違う原因や結果だったけど

①def current_userが2つ定義されていました。
⇛ 1つ削除しました。

の部分を見て自分でも同じように単純に同じ役割で重複しているコードがあるかもしれない?と考えることが出来るようになりました。

11章のuser.rbを確認すると
https://github.com/yasslab/sample_apps/blob/master/5_1_2/ch11/app/models/user.rb

お手本となるコードが以下の通りで

app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  before_save   :downcase_email
  before_create :create_activation_digest


  validates :name,  presence: true, length: { maximum:  50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true,
    length: { minimum: 6 }, allow_nil: true

  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  def User.new_token
    SecureRandom.urlsafe_base64
  end

  def remember
    self.remember_token = User.new_token
    self.update_attribute(:remember_digest,
      User.digest(remember_token))
  end

  def forget
    self.update_attribute(:remember_digest, nil)
  end

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(attribute, token)
    digest = self.send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end

  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

    def downcase_email
      self.email = self.email.downcase
    end

    def create_activation_digest
      self.activation_token  = User.new_token
      self.activation_digest = User.digest(self.activation_token)
      # @user.activation_digest => ハッシュ値
    end
end

その時の自分のリスト11.29までの終了時点でのuser.rbのコードと比較すると

user.rb
class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  before_save   :downcase_email
  before_create :create_activation_digest
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }, allow_nil: true

  # 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

    # トークンがダイジェストと一致したらtrueを返す
    def authenticated?(attribute, token)
      digest = send("#{attribute}_digest")
      return false if digest.nil?
      BCrypt::Password.new(digest).is_password?(token)
    end

  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

  # ユーザーのログイン情報を破棄する
  def forget
    update_attribute(:remember_digest, nil)
  end

  private

  # メールアドレスをすべて小文字にする
  def downcase_email
    self.email = email.downcase
  end

  # 有効化トークンとダイジェストを作成および代入する
  def create_activation_digest
    self.activation_token  = User.new_token
    self.activation_digest = User.digest(activation_token)
  end

  private
  # メールアドレスをすべて小文字にする
  def downcase_email
    # self.email = email.downcase
    email.downcase!
  end
end

お手本のコードと比較するとauthenticated?メソッドは1つしかなく自分のコードを見るとauthenticated?メソッドが2つあって重複していること自体がおかしいのではないか?ということに気付いた。

    # トークンがダイジェストと一致したらtrueを返す
    def authenticated?(attribute, token)
      digest = send("#{attribute}_digest")
      return false if digest.nil?
      BCrypt::Password.new(digest).is_password?(token)
    end




  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

対処法

重複しているコードを削除する。

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

テストの結果

このコードを削除することでGreenとなった。

Pry#input_array is deprecated. Use Pry#input_ring instead
19:57:47 - INFO - Running: test/models/user_test.rb
Running via Spring preloader in process 6751
Started with run options --seed 62215

  12/12: [=========================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.81365s
12 tests, 20 assertions, 0 failures, 0 errors, 0 skips

どうしてこういう初歩的なミスが起きたのか?

リスト 11.26: 抽象化されたauthenticated?メソッド red

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # トークンがダイジェストと一致したらtrueを返す
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end
  .
  .
  .
end

この時点でこの変更点は書き換えでは無く追加だと思ってしまった。

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

このコードがあることに加えて
さらに

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # トークンがダイジェストと一致したらtrueを返す
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end
  .
  .
  .
end

このコードを追記しなければならないと勘違いしてしまったが、
実際は追記するのではなくて

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

上記のコードを

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # トークンがダイジェストと一致したらtrueを返す
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end
  .
  .
  .
end

と書き換えるのがリスト11.26での正しい作業だと気付けなかった。

単純に自分の知識や経験、常識の不足から来るミスだと思うし、同じ名前のメソッドが2つもあることを疑えなかった、おかしいと気付けなかったのが原因だと思う。

ちなみに1周目ではこのエラーを起こさずに普通にこなせていたので、おそらく1周目とは違う考え方で作業やって何事も無くこなせてしまっていたんだと思う。
1周目で起きたトラブルに2周目で対処するとかなら良いことだと思うけど、その逆で1周目の時は無かったエラーやトラブルを2周目でやらかすのはあまり良くないので反省。
1周目では何事も問題無かったのに2周目なら何故起きるんだろう?という手がかりも1周目やった時にはあまり残せていなかったので、その辺ののメモも今後たくさん取っておきたいと思う。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした