【95日目】【1日20分のRailsチュートリアル】【第8章】演習の2.
今日は「8.6 演習」の2.から。
8.6 演習
2. 8.4.6では、現在のアプリケーション設計では、リスト8.51の統合テストで仮想のremember_token属性にアクセスする手段がないことを説明しました。
実は、assignsという特殊なテストメソッドを使用するとアクセスできるようになります。
(中略)
[remember me] チェックボックスのテストを改良してください。
うーん、、何となく分かるような分からないような。。。
とりあえず演習はこなそう。
まずSessionコントローラーで@userというインスタンス変数を定義するように変更。
user
を@user
に置換しただけ。
リスト8.60: class << selfを使ってトークンやダイジェストの新しいメソッドを定義する
app/controllers/sessions_controller.rb
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_to @user else
次は、cookiesにユーザーの記憶トークンが正しく含まれているかどうかをチェックするテストコードを書けばいいっぽい。
リスト8.62: [remember me] テストを改良するためのテンプレート
test/integration/users_login_test.rb
test "login with remembering" do log_in_as(@user, remember_me: '1') assert_equal cookies['remember_token'], assigns(:user).remember_token end
これでいいのかな。。。
テスト実行してテストが通ることは確認。
$ bundle exec rake test 29 tests, 67 assertions, 0 failures, 0 errors, 0 skips
サンプルソースが用意されているからこそできてる演習。まぁいいか。
一応演習は終わり!
今日の学習時間は【23分】。
次は「第9章 ユーザーの更新・表示・削除」から。
やっと9章だー
【94日目】【1日20分のRailsチュートリアル】【第8章】演習の1.
今日は「8.6 演習」から。
8.6 演習
なお、演習とチュートリアル本編の食い違いを避ける方法については、演習用のトピックブランチに追加したメモ (3.6) を参考にしてください。
演習用にブランチ切っておく。
$ git checkout log-in-log-out $ git checkout -b log-in-log-out-exercises
- リスト8.32では、明示的にUserをプレフィックスとして、新しいトークンやダイジェストのクラスメソッドを定義しました。
(中略)
リスト8.59やリスト8.60の文脈では、selfはUser「クラス」を指すことにご注意ください。わかりにくさの原因の一部はこの点にあります)。
うーん、文章長くなってきましたね。。。
「Ruby的に正しい」クラスメソッドの定義方法
について学習する演習っぽい。
リスト8.59: selfを使ってトークンやダイジェストの新しいメソッドを定義する
User.digest(string)
のようにUser
としていたところをself
にしても問題ないことを確認する。
app/models/user.rb
: # 与えられた文字列のハッシュ値を返す def self.digest(string) cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost BCrypt::Password.create(string, cost: cost) end # ランダムなトークンを返す def self.new_token SecureRandom.urlsafe_base64 end :
テスト実行して問題ないことを確認。
$ bundle exec rake test 29 tests, 67 assertions, 0 failures, 0 errors, 0 skips
リスト8.60: class << selfを使ってトークンやダイジェストの新しいメソッドを定義する
class << self
としてUser
を外しても問題ないことを確認する。
app/models/user.rb
: class << self # 与えられた文字列のハッシュ値を返す def digest(string) cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost BCrypt::Password.create(string, cost: cost) end # ランダムなトークンを返す def new_token SecureRandom.urlsafe_base64 end end :
class << self
とend
でdigest
とnew_token
メソッドを囲む感じ。
テスト実行して問題ないことを確認。
$ bundle exec rake test 29 tests, 67 assertions, 0 failures, 0 errors, 0 skips
正直内容の理解はできていないけど…まぁいいか。
Rubyの勉強はまた追々かな。
今日の作業時間は【26分】。
次は「8.6 演習」の2.から。
【93日目】【1日20分のRailsチュートリアル】【第8章】第8章のまとめ
今日は「8.5 最後に」から。
8.5 最後に
次の章に進む前に、変更をmasterブランチにマージしておきましょう。
テストを実行してエラーが出ないことを確認したらコミットしてmasterブランチにマージ。
$ bundle exec rake test 29 tests, 67 assertions, 0 failures, 0 errors, 0 skips $ git add -A $ git commit -m "Finish log in/log out" $ git checkout master $ git merge log-in-log-out
続いて、リモートリポジトリとproductionサーバーにもプッシュします。
masterブランチでも念のためテストを実行して、エラーが出ないことを確認してリモートリポジトリにpush。
$ bundle exec rake test 29 tests, 67 assertions, 0 failures, 0 errors, 0 skips $ git push
リモートリポジトリにpushできました。
Herokuにもpushしておく。
$ git push heroku $ heroku run rake db:migrate
プッシュした後、マイグレーションが完了するまでの間、一時的にステータスが無効 (invalid) になりますので、ご注意ください。
トラフィックの多い本番サイトでは、変更を行う前に以下のようにメンテナンスモードをオンにしておくとよいでしょう。
heroku maintenance:on
でメンテナンスモードをオンにできるそう。
今はチュートリアルだしまぁいいか。。。
8.5.1 本章のまとめ
第8章を開始したのが2016/11/15なので2ヶ月弱かけて第8章を学習してきたことになる。
最初の方忘れてるな…。
cookiesが絡んできて難しく感じた。基本的なWebの知識が必要だよね。。。
今日の作業時間は【12分】。
次は「8.6 演習」から。
【92日目】【1日20分のRailsチュートリアル】【第8章】記憶ブランチをテストする
今日は「8.4.6 Rememberのテスト」の「記憶ブランチをテストする」から。
8.4.6 Rememberのテスト
記憶ブランチをテストする
current_user内のある分岐部分については、これまでまったくテストが行われていないのです。
(中略)わざと例外発生を仕込むという手法を好んで使います。
そのコードブロックがテストから漏れていれば、テストはパスしてしまうはずです。
まずはテストを忘れてる部分に例外発生を仕込む。
app/helpers/sessions_helper.rb
: elsif (user_id = cookies.signed[:user_id]) raise # テストがパスすれば、この部分がテストされていないことがわかる user = User.find_by(id: user_id) :
raise
で例外発生させられるっぽい。
試しにテスト実行してみると通ってしまう。
$ bundle exec rake test 27 tests, 64 assertions, 0 failures, 0 errors, 0 skips
以前作成した以下のSessionsヘルパーのテストでcurrent_userを直接テストすれば、この制約を突破できます。
以前作成したってどのテストだ。。。まぁいいや。
テスト手順はシンプルです。
1. フィクスチャでuser変数を定義する
2. 渡されたユーザーをrememberメソッドで記憶する
3. current_userが、渡されたユーザーと同じであることを確認します。
テストファイルを作成して
$ touch test/helpers/sessions_helper_test.rb
下記のように更新。
test/helpers/sessions_helper_test.rb
require 'test_helper' class SessionsHelperTest < ActionView::TestCase def setup @user = users(:michael) remember(@user) end test "current_user returns right user when session is nil" do assert_equal @user, current_user assert is_logged_in? end test "current_user returns nil when remember digest is wrong" do @user.update_attribute(:remember_digest, User.digest(User.new_token)) assert_nil current_user end end
@userインスタンスとcurrent_userが一緒であればログインされていることを確認するテスト、
Userの記憶ダイジェストと記憶トークン異なっていればcurrent_userがnilになることを確認するテストを追加。
5.6で簡単に触れたように、アサーションassert_equalの引数は、期待する値、実際の値の順序で書くのがルールになっています。
assert_equal <期待する値>, <実際の値>
へー。そうなのか。
今度は期待通りにテストNGとなることを確認。
$ bundle exec rake test TEST=test/helpers/sessions_helper_test.rb 2 tests, 0 assertions, 0 failures, 2 errors, 0 skips
ここまでできれば、current_userメソッドに仕込んだraiseを削除して元に戻す (リスト8.57) ことで、リスト8.55のテストがパスするはずです 。
app/helpers/sessions_helper.rb
に追加したraiseを削除して、テストが通ることを確認。
$ bundle exec rake test 29 tests, 67 assertions, 0 failures, 0 errors, 0 skips
分かっていないところもあるけど足早に終わらせました。
一回やるだけで完璧に理解は難しいだろうなぁ。
今日の作業時間は【20分】。
次は「8.5 最後に」から。
やっと8章の終わりが見えてきた!
【91日目】【1日20分のRailsチュートリアル】【第8章】“Remember me”チェックボックスをテストする
今日は「8.4.6 Rememberのテスト」から。
8.4.6 Rememberのテスト
しかしもっと重要な理由は、ユーザーを永続化するコードの中心部分が、実はまだまったくテストされていないからです。
そうだっけ…そうかもしれない。。。
ユーザーの永続化によって見た目がどう変わるか、リンクテキストはどうなるか、のテストだったような気もする。
[remember me] ボックスをテストする
リスト8.20では、postメソッドと有効なsessionハッシュを使用してログインしましたが、毎回このようなことをするのは面倒です。
そこで、log_in_asというヘルパーメソッドを作成してテスト用にログインできるようにし、無駄な繰り返しを排除します。
うぅーん、、なんだかログイン処理のテストって難しいね。。。解説を理解するのに時間かかる…。
統合テストの内部では、リスト8.20のようにセッションパスをpostしますが、コントローラやモデルなどの単体テストでは同じ方法が使えません (セッションがないからです)。
分かるような分からないような感じだけど統合テストと単体テストでコードを分ける必要があるんですね。
test/test_helper.rb
# テストユーザーとしてログインする def log_in_as(user, options = {}) password = options[:password] || 'password' remember_me = options[:remember_me] || '1' if integration_test? post login_path, session: { email: user.email, password: password, remember_me: remember_me } else session[:user_id] = user.id end end private # 統合テスト内ではtrueを返す def integration_test? defined?(post_via_redirect) end
まずdefined?(post_via_redirect)
で統合テストかどうかが分かるのでメソッド化して、
統合テストであればPOSTメソッドでログイン処理して、
統合テストじゃなければセッションを作成するような感じか。。。
cookiesの値がユーザーの記憶トークンと一致することを確認できれば理想的なのですが、現在の設計ではテストでこの確認を行うことはできません。
テスト用の@userインスタンスにはremember_tokenが含まれていないそう。
この辺よく分からないけど、、、まぁいいか。
さしあたって、今は関連するcookiesがnilであるかどうかだけをチェックすればよいことにします。
チェックボックスがオンであればcookiesがnilじゃないこと、
チェックボックスがオフであればcookiesがnilなことを確認する。
test/integration/users_login_test.rb
test "login with remembering" do log_in_as(@user, remember_me: '1') assert_not_nil cookies['remember_token'] end test "login without remembering" do log_in_as(@user, remember_me: '0') assert_nil cookies['remember_token'] end
テスト実行して問題ないことを確認。
$ bundle exec rake test 27 tests, 64 assertions, 0 failures, 0 errors, 0 skips
ユーザー関連のテスト難しいわ。。。
今日の作業時間は【35分】。
次は「8.4.6 Rememberのテスト」の「記憶ブランチをテストする」から。
【90日目】【1日20分のRailsチュートリアル】【第8章】“Remember me”チェックボックスを追加する
今日は「8.4.5 “Remember me” チェックボックス」から。
8.4.5 “Remember me” チェックボックス
今回の実装は、リスト8.2のログインフォームにチェックボックスを追加するところから始めます。
ログインフォームのビューにチェックボックスを追加。
app/views/sessions/new.html.erb
<%= f.label :remember_me, class: "checkbox inline" do %> <%= f.check_box :remember_me %> <span>Remember me on this computer</span> <% end %>
見た目を整えるためにCSS追加。
app/assets/stylesheets/custom.css.scss
.checkbox { margin-top: -10px; margin-bottom: 10px; span { margin-left: 20px; font-weight: normal; } } #session_remember_me { width: auto; margin-left: 0; }
サーバーを起動して
$ rails server -b $IP -p $PORT
チェックボックスが追加されていることを確認。
ログインフォームの編集が終わったので、チェックボックスがオンのときにユーザーを記憶し、オフのときには記憶しないようにします。
params[:session][:remember_me]
でチェックボックスの状態を取得できるそう。
セッションのcreateアクションにてチェックボックスの状態によってユーザーを覚えておくか忘れるか処理を分ける。
app/controllers/sessions_controller.rb
: if user && user.authenticate(params[:session][:password]) log_in user params[:session][:remember_me] == '1' ? remember(user) : forget(user) redirect_to user :
チェックボックスがオフのときはforget
じゃなくて何もしなくてもいいんじゃないの…?
と思ったけど、前回のログイン時にチェックボックスをオンにしてたら永続的セッションに残ったままになるからわざわざ削除してるんだろうな。
なるほど。
今日の作業時間は【20分】。
次は「8.4.6 Rememberのテスト」から。
【89日目】【1日20分のRailsチュートリアル】【第8章】2つの目立たないバグに対するテストを作成する
今日は「8.4.4 2つの目立たないバグ」のテストを書くところから。
8.4.4 2つの目立たないバグ
テスト駆動開発は、この種の地味なバグ修正にはうってつけです。そこで、2つのエラーをキャッチするテストを書くことにします。
こんなややしこしそうなバグもテスト書けるのか。。。
ログイン→ログアウトのテストに下記コードを追加。
test/integration/users_login_test.rb
# 2番目のウィンドウでログアウトをクリックするユーザーをシミュレートする
delete logout_path
ログアウトした後に再度ログアウトしようとするコード、ってことかな…?
$ bundle exec rake test 24 tests, 58 assertions, 0 failures, 1 errors, 0 skips
テストはもちろんNGです。
リスト8.42のアプリケーションコードでは、logged_in?がtrueの場合に限ってlog_outを呼び出すように変更しました。
destroyアクションを下記のように変更。
app/controllers/sessions_controller.rb
def destroy log_out if logged_in? redirect_to root_url end
$ bundle exec rake test 24 tests, 61 assertions, 0 failures, 0 errors, 0 skips
テスト成功しました。
2番目の問題についてですが、統合テストで2種類のブラウザをシミュレートするのは正直かなり困難です。その代わり、同じ問題をUserモデルで直接テストするだけなら簡単に行えます。
ほぅほぅ。
Userモデルのテストとして下記を追加。
test/models/user_test.rb
test "authenticated? should return false for a user with nil digest" do assert_not @user.authenticated?('') end
テストはNGになります。
$ bundle exec rake test ERROR["test_authenticated?_should_return_false_for_a_user_with_nil_digest", UserTest, 2016-11-22 15:03:43 +0000] test_authenticated?_should_return_false_for_a_user_with_nil_digest#UserTest (1479827023.00s) BCrypt::Errors::InvalidHash: BCrypt::Errors::InvalidHash: invalid hash 25 tests, 61 assertions, 0 failures, 1 errors, 0 skips
エラーを修正してテストがGREENになるようにするには、記憶ダイジェストがnilの場合にfalseを返すようにすればよいのです (リスト8.45)。
authenticated?
メソッドに記憶トークンがnilの場合はreturnするコードを追加。
app/models/user.rb
# 渡されたトークンがダイジェストと一致したらtrueを返す def authenticated?(remember_token) return false if remember_digest.nil? BCrypt::Password.new(remember_digest).is_password?(remember_token) end
これでテストが通るようになり、2つの目立たないバグも解消されました。
$ bundle exec rake test 25 tests, 62 assertions, 0 failures, 0 errors, 0 skips
テストにも色んなアプローチがあるんだな。。。
今日の作業時間は【22分】。
次は「8.4.5 “Remember me” チェックボックス」から。