railsチュートリアル9章が終わったので、難しかったところをまとめていきたいと思います。
リスト9.3の
# 永続セッションのためにユーザーをデータベースに記憶する
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
初め、このコードをどうやって読めばいいのか分かりませんでした。
どのような挙動をしているか、よく把握できなかった感じです。
二つ目のコード、
update_attribute(:remember_digest, User.digest(remember_token))
は、どういうふうに読めばいいかと言うと、「:remember_digestをUser.digest(remember_token)に更新している(アップデートしている)」というわけですね。
このコード全体の流れを追いかけると、def rememberが発動することによって、User.new_tokenが呼び出され、その結果がremember_tokenへと代入されます。
このremember_tokenはローカル変数ではなく属性なので、selfが必要です。そして、そのremember_tokenがUser.digest(string)の、「string」のところに入れられて、その結果が:remember_digestへと代入されるわけです。
次に難しかったのが、9.1.2のこのコードです。
BCrypt::Password.new(remember_digest).is_password?(remember_token)
どうして値の異なるものを比較して同じ(一致している)だとみなせるのか、文章の意味がよく分からなかったです。つまり、復号化(暗号されているものを元に戻すこと)しているわけでもなく、復号化したところでremember_digestと remember_tokenは異なる値です。それなのに一致しているとはどういうことか。
ここで図9.1を見てみます。この図においては、Userモデルにremember_digest属性が追加されています。つまり、データベースに保存されているのは、remember_digestというわけです。
そして、ブラウザの方にcookiesとして記憶されるのが、remember_tokenというわけです。リスト9.2の下の説明にもあるように、「トークンをデータベースに保存せずに実装する必要があります」というわけです。
データベースに保存されている「暗号化されたremember_digest」、ブラウザに記憶されている「(cookiesによって)暗号化されたremember_token」。
この二つが同じIDから生まれたものなのかどうかを比較するために使われるのが、BCrypt::Password.new(remember_digest).is_password?(remember_token)のコードである、というわけです。
より分かりやすく言えば、これは日本語の問題です。
「一致」という言葉はここでは、「同じ値になる」を意味するわけではなく、「一致するIDから生まれた値である」を意味しているというわけです。
次はリスト9.8です。リスト9.8の中の
# ユーザーのセッションを永続的にする
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
このコードです。
このコードがいったいどんな挙動をするか、日本語で書いていきたいと思います。
①remember(user)のuserの部分にユーザーが渡されることで発動する(※リスト9.7ではuserの前後ろに丸括弧はない)。
②user.rememberによって、リスト9.6などのdef rememberが発動して、remember_tokenとremember_digestが生成される。
③user.idがcookies.permanent.signed[:user_id] へと代入される。暗号化および永続化もされる。
④先程作られたremenber_tokenがcookies.permanent[:remember_token]へと代入される。暗号化および永続化もされる。
つまり上のコードは、結果をcokkiesへと保存する処理になります。
Cookieとセッションの違いについてもふと気になったので調べてみました。
なんとなく学んだ感じだと、Cookieのほうが期間が長くて、セッションのほうが期間が短い。Cookieはブラウザを閉じても保持されて、セッションはブラウザを閉じると消されるもの、という印象があります。
クッキー(Cookie)とセッションとキャッシュの違いは何か? | Promapedia
https://ssaits.jp/promapedia/technology/cookie-session-cache.html
などの記事も読みました。こちらのほうが詳細ですが、やはり上のような認識で大丈夫そうです。
次にぶち当たった難所は、リスト9.17です。このハイライトが何を意味しているのか、よく掴めませんでした。
これは、@user.authenticated?('')は、丸括弧の中に何も入っていないので、必ずnilになります。そしてnilのままテストをすると、「assert_not nil」となってしまい、結果としてerrorになってしまいます。
「assert_notは、右側にtrueかfalseが来ないと、テストがエラーになってしまう」ということです。
なので、nilになったときにfalseを返すようにするため、リスト9.19のハイライトが追加されているというわけです。
次は統合テストです。
統合テストとは何でしょう? よく分かりません。なので調べてみました。
単体テスト・結合テスト・総合テストの違い、観点や注意点を簡単に説明する | 若手エンジニアの羅針盤
https://pm-rasinban.com/ut-it-st
ふむふむ。なるほど、やっぱりよく分かりません。(*_*)
自分なりに咀嚼して考えると、
・ひとつの機能だけを調べるのが、単体テスト。
・二つ以上のテストを連動させ、まとめて調べるのが統合テスト(結合テスト)
みたいな感じっぽいです。
次にリスト9.25です。
この下のハイライトの部分がよく分かりませんでした。
test "login without remembering" do
# クッキーを保存してログイン
log_in_as(@user, remember_me: '1')
delete logout_path
# クッキーを削除してログイン
log_in_as(@user, remember_me: '0')
assert_empty cookies['remember_token']
end
ひとつのテストのなかで2回ログインしている。これはどういうことなのでしょうか?
で、しばらく考えて気づきました。
まず、log_in_as(@user, remember_me: '1')でクッキーを保存してログインしています。そして2行目のdelete logout_pathで、一旦ログアウトしています。ここでログアウトしているからこそ、3行目 log_in_as(@user, remember_me: '0')でログインできてます。最後に、クッキーを保存せずにログインしていることを確認するために、assert_empty cookies['remember_token']で確認しています。
delete logout_pathはルーティングで学んだ通り、ログアウトするためのコードです。
クッキー有りでログインする→ログアウトする→クッキー無しでログインする→クッキーが無いことを確認する、という流れですね。
いったんログアウトをすることで、同じテストのなかでもう一度ログインすることができている、というわけでした。
リスト9.24とリスト9.25の間の文章で、よく分からないところがありました。
コントローラ内のuser変数には記憶トークンの属性が含まれていますが、remember_tokenは実在しない「仮想」のものなので、@userインスタンス変数の方には含まれていません。
remember_tokenが「実在しない仮想のものである」とは一体どういうことなのでしょうか?
上のほうまで遡って、「リスト9.2 トークン生成用メソッドを追加する」の下の文章を参照すると、
トークンに対応する記憶ダイジェストをデータベースに保存します。リスト 9.1のマイグレーションは実行済みなので、Userモデルには既にremember_digest属性が追加されていますが、remember_token属性はまだ追加されていません。このためuser.remember_tokenメソッドを使ってトークンにアクセスできるようにし、かつ、トークンをデータベースに保存せずに実装する必要があります。そこで、6.3で行ったパスワードの実装と同様の手法でこれを解決します。
と書かれています。
つまり、「実在しない仮想のものである」とは、「データベースに保存されない」を意味していることが分かります。
そしてこのデータベースに保存されないモノに関しては、そのままの状態ではインスタンス変数の場合だと、確認することができないらしいです。インスタンス変数は、データベースに保存されるものは確認できるので、結果的に保存されるCookieなどは確認できます。しかし、〝過程〟で生成される仮想の属性に関しては、そのままだと確認できないのです。
そして、この仮想の属性にアクセスするために使われるのが、「assigns」というメソッドです。
assignsメソッドを使うことによって、テストでも「過程で生成される仮想の属性」を確認できるようになるというわけです。
また、assignsメソッドを使うためには、「createアクション(新たにユーザを生成するアクション)」において、@userというインスタンス変数を定義する必要があります。
9.3.2[Remember Me]のテストをする、の章に入ります。
このリスト9.29に出てくる「raise」とは何でしょうか?
raiseとは、例外処理のことで、意図的にエラーを引き起こすものです。これを使うと、テストの進行を止めることができます。
もし、テストがGREENのままであるならば、このraiseの部分を通過しなかったということになるので、そのコードブロック(一つの文章のまとまり)は、テストから漏れていることが分かります。
さて、上の問題を解決するために、リスト9.31のようなコードを書くわけですが、これがとても難しいです。順を追って解説していきたいと思います。
まず、def setupでセッションが設定されていないuser変数を定義します。
@userにmichealのデータを入れて、rememberメソッドでtokenやdigestやcookiesを生成するわけですね(rememberメソッドは、リスト9.8で作成したものです)。
このusers(:micheal)ってなんだったかな?と思い出せない人もいるかも知れません。自分もすっかり忘れていました。これはリスト9.25の上にある文章で、「リスト8.22」を参照にしてください、と書いてあるので、リスト8.22に飛びます。
リスト8.22ではfixtureと呼ばれるものが定義されています。これは、いろいろなテストで参照できるuserのことです。なので、ここでfixtureとして定義したmichealくんを、9章でも使えるというわけです。
さて、このあとに二つのテストをおこなう必要があります。
① test "current_user returns right user when session is nil" do
② test "current_user returns nil when remember digest is wrong" do
この二つのテストのコードは、それぞれ別々に作動するものです。①のテストが終わったあとに、改めて②のテストがおこなわれる、というわけです。
なので、①と②が同時におこなわれたり、連結しているわけではないのでご注意を(順番にテストがおこなわれる、というだけです)。
→testの中身はそれぞれの項目が別々に実施される。test~doまでがひとつのブロック
さて、それぞれのテストの中身ですが、①のほうは分かりやすいです。
リスト9.29の「if user && user.authenticated?(cookies[:remember_token])」が通るので、その下の@current_user=userもおこなわれるので、テストも完了するというわけです。
しかし、②はどうでしょう。これは日本語に直せば、「remember_digestが間違っていたら、current_userはnilを返すのを確認するテストだよ」です。
さて、その下の、「@user.update_attribute(:remember_digest, User.digest(User.new_token))」はいったい何をおこなっているのでしょうか?
ここでかなり悩んでしまいましたが、実はそんなに難しくないのです。
これは、簡単に言えばアップデート……つまり上書きをおこなっているわけです。
そして、上書きされた値は、def setupで生成されたremember_digestとは違うものになっています。
上のコードを日本語に直すと、「@userのremember_digestをUser.digest(User.new_token)で上書きするよ。このUser.digest(User.new_token)は、newが入っているので、新たに生成されたものだよ」ということです。
そしてここでアップデートされたものは、cookiesには反映されていません。データベースの方はアップデートされていますが、cookiesはアップデートされていないというわけです。ですから、remember_digestがwrongになるというわけです。
すると、current_userには何も代入されないため、nilになります。よって、assert_nilを通過して、テストにパスするわけですね。
(digestが違うわけですから、もちろんログインも出来ません←リスト9.33)
ちなみにここの部分は難しかったので、この記事を参考にしました。英語です。
ruby on rails - test "current_user returns nil when remember digest is wrong" - Stack Overflow
https://stackoverflow.com/questions/39881313/test-current-user-returns-nil-when-remember-digest-is-wrong
stack overflow、素晴らしいですね。分かりやすいですし、英語の勉強にもなります。
ちなみに、グーグルで「test "current_user returns nil when remember digest is wrong" do」で検索して、一番最初にヒットした記事です。分からない部分があったら、そこの文章やコードをまるごと検索してみると、解説が見つかったりします。
他に参考にした記事も明記していきます。
【Ruby】例外処理まとめ - Qiita
https://qiita.com/k-penguin-sato/items/1a6c8096effccb19565a
Ruby on Railsのtrue/false まとめ - Qiita
https://qiita.com/18sasaki/items/514963c8bd3ae68e923c
【Ruby】 attr_accessorメソッドの使い方と必要な理由とは? | Pikawaka - ピカ1わかりやすいプログラミング用語サイト
https://pikawaka.com/ruby/attr_accessor
rails tutorial 9章 簡易まとめ - Qiita
https://qiita.com/krppppp/items/e266e87b8b6caeb22204
Railsチュートリアル第9章まとめ - Qiita
https://qiita.com/s_rkamot/items/bd06027945473e33de30
【Rails】cookiesによる永続的なログイン【Rails Tutorial 9章まとめ】 - Qiita
https://qiita.com/kagamiya9/items/07d185dedfb31a48d509
35歳だけどRailsチュートリアルやってみた。[第4版 8章 8.3 ログアウト まとめ&解答例] - Qiita
https://qiita.com/yokoyan/items/1f1ea43a691df5edbc0b
railsチュートリアル9章で難しかったポイントは以上になります。
明日からは10章をやっていきたいと思います。がんばります。