Activation test and refactoring

這節要撰寫帳號啟動的整合測試,之前已經為註冊寫過一個測試,我們會在那個測試加幾個步驟:

test/integration/users_signup_test.rb

require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest

  def setup
    ActionMailer::Base.deliveries.clear
  end

  test "invalid signup information" do
    get signup_path
    assert_no_difference 'User.count' do
      post users_path, user: { name:  "",
                               email: "user@invalid",
                               password:              "foo",
                               password_confirmation: "bar" }
    end
    assert_template 'users/new'
    assert_select 'div#error_explanation'
    assert_select 'div.field_with_errors'
  end

  test "valid signup information with account activation" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, user: { name:  "Example User",
                               email: "[email protected]",
                               password:              "password",
                               password_confirmation: "password" }
    end
    assert_equal 1, ActionMailer::Base.deliveries.size
    user = assigns(:user)
    assert_not user.activated?
    # Try to log in before activation.
    log_in_as(user)
    assert_not is_logged_in?
    # Invalid activation token
    get edit_account_activation_path("invalid token")
    assert_not is_logged_in?
    # Valid token, wrong email
    get edit_account_activation_path(user.activation_token, email: 'wrong')
    assert_not is_logged_in?
    # Valid activation token
    get edit_account_activation_path(user.activation_token, email: user.email)
    assert user.reload.activated?
    follow_redirect!
    assert_template 'users/show'
    assert is_logged_in?
  end
end

其中這行:

assert_equal 1, ActionMailer::Base.deliveries.size

是確認只發送一封郵件,因為 deliveries 是全域陣列,會統計所有發出的郵件,所以要在 setup 重設它,以防其他測試發送了郵件(之後會提到)。我們也第一次使用了 assigns 方法,它的用途是在相對應的 action 中獲取實例變數。

例如,Users controller 中的 create action 定義了一個 @user 變數,所以可以在測試中使用 assigns(:user) 來取得這個變數。

注意,我們也把之前的註解拿掉了。

執行測試,應該會通過(Green):

$ bundle exec rake test

寫完測試後,就可以進行重構,我們要把某些 controller 裡處理使用者行為的程式碼移到 model 裡。我們會定義一個 activate 方法,來更新使用者的啟動狀態;還要定義一個 send_activation_email 方法,用來寄出啟動郵件。

以下是重構的程式碼:

app/models/user.rb

class User < ActiveRecord::Base
  .
  .
  .
  # Activates an account.
  def activate
    update_attribute(:activated,    true)
    update_attribute(:activated_at, Time.zone.now)
  end

  # Sends activation email.
  def send_activation_email
    UserMailer.account_activation(self).deliver_now
  end

  private
    .
    .
    .
end

app/controllers/users_controller.rb

class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      @user.send_activation_email
      flash[:info] = "Please check your email to activate your account."
      redirect_to root_url
    else
      render 'new'
    end
  end
  .
  .
  .
end

app/controllers/account_activations_controller.rb

class AccountActivationsController < ApplicationController

  def edit
    user = User.find_by(email: params[:email])
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
      user.activate
      log_in user
      flash[:success] = "Account activated!"
      redirect_to user
    else
      flash[:danger] = "Invalid activation link"
      redirect_to root_url
    end
  end
end

注意,在 app/models/user.rb 中沒有使用 user. 因為 User model 沒有這個變數:

-user.update_attribute(:activated,    true)
-user.update_attribute(:activated_at, Time.zone.now)
+update_attribute(:activated,    true)
+update_attribute(:activated_at, Time.zone.now)

也可以把 user 改成 self,但之前提過不能在 model 裡加 self。同時,在調用 UserMailer 時,把 @user 改成 self

-UserMailer.account_activation(@user).deliver_now
+UserMailer.account_activation(self).deliver_now

就算是簡單的重構,也可能忽略這樣的細節,所以好的測試能捕捉這些問題,現在執行測試應該會通過(Green):

$ bundle exec rake test

啟動帳號完成了,先來提交一下:

$ git add -A
$ git commit -m "Add account activations"