ぞえの技術めも

Ruby on Rails勉強中

【95日目】【1日20分のRailsチュートリアル】【第8章】演習の2.

Ruby on Railsチュートリアル(第3版)

今日は「8.6 演習」の2.から。

8.6 演習

railstutorial.jp

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.

Ruby on Railsチュートリアル(第3版)

今日は「8.6 演習」から。

8.6 演習

railstutorial.jp

なお、演習とチュートリアル本編の食い違いを避ける方法については、演習用のトピックブランチに追加したメモ (3.6) を参考にしてください。

演習用にブランチ切っておく。

$ git checkout log-in-log-out
$ git checkout -b log-in-log-out-exercises                                                                  
  1. リスト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 << selfenddigestnew_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章のまとめ

Ruby on Railsチュートリアル(第3版)

今日は「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

f:id:kt_zoe:20170105123353p:plain

リモートリポジトリに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章】記憶ブランチをテストする

Ruby on Railsチュートリアル(第3版)

今日は「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”チェックボックスをテストする

Ruby on Railsチュートリアル(第3版)

今日は「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”チェックボックスを追加する

Ruby on Railsチュートリアル(第3版)

今日は「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

チェックボックスが追加されていることを確認。

f:id:kt_zoe:20161226123715p:plain

ログインフォームの編集が終わったので、チェックボックスがオンのときにユーザーを記憶し、オフのときには記憶しないようにします。

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つの目立たないバグに対するテストを作成する

Ruby on Railsチュートリアル(第3版)

今日は「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” チェックボックス」から。