Remember tests
雖然 Remember me 的功能已經可以運作了,但還是要寫個測試來檢查它的運作。測試可以捕捉實作時可能產生的錯誤,另外就是我們還沒針對持久性 sessions 做測試。
Testing the “remember me” box
在處理 checkbox 的時候,程式碼如下:
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
但最初作者寫成這樣:
params[:session][:remember_me] ? remember(user) : forget(user)
params[:session][:remember_me] 的值不是 0 就是 1,都是真值,所以都會回傳 true,這表示 APP 會一直以為勾選了 checkbox,這就是測試能捕捉的問題點。
因為記住使用者的條件就是必須為登入狀態,我們的第一步就是建立一個輔助方法,放進測試裡,作用是要登入使用者。
在之前,我們透過使用 post 方法和一個 session hash 來登入使用者進行測試:
post login_path, session: { email: @user.email, password: 'password' }
但如果每次都要重複這樣寫太麻煩,所以我們要定義一個 log_in_as 輔助方法來處理登入。而這個方法會取決於測試的類型,在整合測試裡面,我們可以使用 post 方法發送 session hash 登入使用者,不過在其他類型的測試,例如 controller test 和 model test,就無法這樣做了,必須直接使用 session 方法。
所以 log_in_as 輔助方法必須進行判斷的動作,針對不同測試類型,去調用適合的方法。這時候可以使用 Ruby 的 defined? 方法來區分整合測試或其他類型的測試。defined? 的參數如果有被定義,會回傳 true,反之回傳 false。
例如,前面的 post_via_redirect 方法只在整合測試中有用,所以:
defined?(post_via_redirect) ...
會在整合測試中回傳 true,在其他類型的測試回傳 false。
由此,我們可以定義一個 integration_test? 布林值方法,然後使用 if..then 述句撰寫下列程式碼:
if integration_test?
# Log in by posting to the sessions path
else
# Log in using the session
end
把上面的註解換成程式碼之後的 log_in_as 輔助方法如下:
test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
fixtures :all
# Returns true if a test user is logged in.
def is_logged_in?
!session[:user_id].nil?
end
# Logs in a test user.
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
# Returns true inside an integration test.
def integration_test?
defined?(post_via_redirect)
end
end
注意,為了更有彈性,log_in_as 方法接受一個 options hash,而且為 password 和 remember me 設定預設值,分別是「password」和「1」。
因為在 hash 中,如果 key 不存在,就會回傳 nil,所以:
remember_me = options[:remember_me] || '1'
如果傳入了參數,就使用那個值,否則就用預設值。
為了檢查 remember me 的 checkbox 行為,我們會進行兩個測試,一個就是有勾選,另一個沒勾選。所以定義了上面的 log_in_as 輔助方法,就可以很輕鬆的拿來進行測試:
log_in_as(@user, remember_me: '1')
log_in_as(@user, remember_me: '0')
其實參數
1因為是預設值,所以可以省略,只是寫出來脈絡會更清楚。
登入使用者之後,就要透過查找 cookies 裡面有沒有 remember_token 這樣的 key 存在,來檢查使用者是否有被記住。理想情況下,可以檢查 cookie 中的值是否等於使用者的 remember token,但對目前的設計方式來說,無法執行。因為在 controller 中的 user 變數有 remember_token 屬性,但在測試中的 @user 變數卻沒有該屬性(因為該屬性是虛擬屬性)。
這個小缺陷留在練習題實作。現在我們只測試 cookie 相關的值是否為 nil。
還有個小問題,基於某種原因在測試裡面,cookies 方法無法使用 symbol 作為 key:
cookies[:remember_token]
這樣的結果,會總是為 nil。還好,cookies 方法可以接受字串為 key:
cookies['remember_token']
就可以找到我們要的 key 和 value。
最後測試程式碼如下:
test/integration/users_login_test.rb
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
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
end
現在測試應該可以通過(Green):
$ bundle exec rake test
Testing the remember branch
在之前我們手動實作了可以正常使用的持久性 session,但是 current_user 方法的相關分支(relevant branch)還沒有測試過。針對這種情況,可以在未測試的程式碼中拋出例外(raise an exception),如果沒有涵蓋這部分的測試程式碼,測試能通過,如果涵蓋了,失敗的訊息中會標示出相對應的測試。
在未測試的程式碼中拋出例外:
app/helpers/sessions_helper.rb
module SessionsHelper
.
.
.
# Returns the user corresponding to the remember token cookie.
def current_user
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
raise # The tests still pass, so this branch is currently untested.
user = User.find_by(id: user_id)
if user && user.authenticated?(cookies[:remember_token])
log_in user
@current_user = user
end
end
end
.
.
.
end
現在執行測試,會顯示通過(Green):
$ bundle exec rake test
所以表示有問題,上述的程式碼簡單來說壞掉了。此外,手動測試持久性 session 很麻煩,所以如果之後要重構 current_user 的話,現在就要先寫個測試。
因為剛剛定義的 log_in_as 輔助方法會自動設定 session[:user_id],所以在整合測試中要測試 current_user 方法的「remember」分支很難。還好,我們可以跳過這個限制,透過在 Sessions helper test 直接測試 current_user 方法,為此我們要建立一個檔案:
$ touch test/helpers/sessions_helper_test.rb
測試的步驟很簡單:
- 透過 fixtures 定義
user變數 - 調用
remember方法來記住這個使用者 - 檢查
current_user是否和這個使用者相等
因為 remember 方法沒有定義 session[:user_id],所以上述步驟能測試「remember」分支。
測試持久性 session:
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
注意,上面還寫了第二個測試,檢查如果 remember digest 和 remember token 不匹配時,目前使用者會是 nil。這是要測試 authenticated? 方法裡面的 if 巢狀述句:
if user && user.authenticated?(cookies[:remember_token])
順便說一下,上述程式碼中的 assert_equal @user, current_user 也可以寫成:
assert_equal current_user, @user
不過按照慣例,assert_equal 的參數順序習慣使用 expected, actual:
assert_equal <expected>, <actual>
現在執行測試,可以得到我們想要的失敗測試(Red):
$ bundle exec rake test TEST=test/helpers/sessions_helper_test.rb
所以我們要把 current_user 方法恢復原樣,刪除 raise,這樣測試就會通過了(Green):
app/helpers/sessions_helper.rb
module SessionsHelper
.
.
.
# Returns the user corresponding to the remember token cookie.
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])
log_in user
@current_user = user
end
end
end
.
.
.
end
再度執行測試,顯示通過(Green):
$ bundle exec rake test
現在 current_user 的「remember」分支已經有了測試,不用手動檢查了,還能進行測試和捕捉 regressions。