測試 Model

關於 Rails Model 請參考這篇文章 Action Mailer Basics

Model Spec 是最簡單撰寫,速度最快的測試。

以下介紹 Model 常見的測試標的。

命名慣例

文件擺放位置:

spec/models/[model_name]_spec.rb

譬如:

app/models/application_record.rb 對應的測試文件為 spec/models/application_record_spec.rb

app/models/user.rb 對應的測試文件為 spec/models/user_spec.rb

文件結構

測試按照 Model 方法所撰寫的順序,以下是我 Model 所採用的順序:

# class User < ActiveRecord::Base if Rails 4
class User < ApplicationRecord
  # 常數
  # 關聯
  # 驗證
  # Scope
  # 類別方法
  # 實體方法
end

測試按這些方法的撰寫順序來測:

require "rails_helper"

RSpec.describe User do
  # constant specs go here
  # associations specs go here
  # validations specs go here
  # scope specs go here
  # class method specs go here
  # instance method specs go here
end

require "rails_helper" 非常重要,這樣才會把 Rails,以及相關的設定(spec/rails_helper.rb)都載入進來。

T> 測試的順序建議跟 Model 方法的順序相同,這樣當測試檔案行數變多的時候,才容易找到測試的位置,讀你程式的人也比較好讀。重點是要保持一致性

輔助工具

安裝 Shoulda Matchers

Shoulda Matchers (https://github.com/thoughtbot/shoulda-matchers) 提供了一系列測試 Active Model、Active Record、Action Controller 方法的匹配語法。

shoulda-matchers gem 安裝到測試環境即可。

group :test do
  gem "shoulda-matchers"
end

加入 shoulda-matchers 需要的設定檔:

# spec/support/shoulda_matchers.rb
Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :rspec
    with.library :rails
  end
end

# 別忘記要在 `spec/rails_helper.rb` 引入 `spec/support/` 目錄下的所有檔案:
Dir["spec/support/**/*.rb"].each { |f| require f }

要測什麼

以下依序介紹。

測關聯

shoulda-matchers 來測試關聯之間是否有正確設定,確保關聯重要的設置不會被改動。

require "rails_helper"

describe User do
  it { should have_many(:repos).through(:memberships) }
  it { should have_many(:subscribed_repos).through(:subscriptions) }
  it { should validate_presence_of :github_username }
  it { should have_many(:memberships).dependent(:destroy) }

  ...
end

範例來源:https://git.io/v6JQh

還可以把這些一行相關的測試包在一個 context 裡:

context "associations" do
  it { should have_many(:repos).through(:memberships) }
  it { should have_many(:subscribed_repos).through(:subscriptions) }
  it { should validate_presence_of :github_username }
  it { should have_many(:memberships).dependent(:destroy) }
end

這樣在跑測試用(--format documentation)參數時,會輸出更明確的信息。

測驗證

用 shoulda-matchers 可以用一行測試常規的 Rails 驗證。但無法測試唯一性驗證,可以自行撰寫測試,以下是一個例子。

# spec/factories.rb
FactoryGirl.define do
  sequence(:github_name) { |n| "github_name#{n}" }

  factory :user do
    github_username { |n| generate(:github_name) }
  end
end

# app/models/user.rb
class User < ApplicationRecord
  validates_presence_of :github_username
  validates_uniqueness_of :github_username
end

# spec/models/user_spec.rb
RSpec.describe User do
  context "validations" do
    it { should validate_presence_of(:github_username) }

    it "works" do
      user = build(:user)

      expect(user).to validate_uniqueness_of(:github_username)
    end
  end
end

測 Scope、類別方法、實體方法

所有公開的方法都應該要測試,Model 之間依靠公開方法進行互動,寫測試檢查行為是否正確如預期,測試在新增功能時可抓 Regression,重構時可確保沒有改壞任何東西。

測試 Scope

class User < ApplicationRecord

end

測試類別方法

以下是一個簡化過的範例,測試一個用來找到已存在或是新增使用者的方法。

# github_info is a hash like this:
#
# {
#   github_username: github_username,
#   email: github_email,
# }

class User < ApplicationRecord
  def self.find_or_create_with(github_info)
    user = find_by("github_username ILIKE ?", github_info[:github_username]) || User.new
    user.update(github_info)
    user
  end
end

ILIKE:不分大小寫比對。

測試:

RSpec.describe User do
  describe ".find_or_create_with" do
    context "new user" do
      it "creates user with github_username" do
        github_info = {
          github_username: "Xdite",
          email: "[email protected]",
        }

        user = User.find_or_create_with(github_info)
        user.reload

        expect(User.count).to eq 1
        expect(user.github_username).to eq "Xdite"
        expect(user.email).to eq "[email protected]"
      end
    end

    context "with existing github_username" do
      it "updates attributes" do
        create(
          :user,
          github_username: "xdite",
          email: "[email protected]",
        )

        github_info = {
          github_username: "Xdite",
          email: "[email protected]",
        }

        user = User.find_or_create_with(github_info)
        user.reload

        expect(User.count).to eq 1
        # 除了上例一個一個 attribute,檢查,也可以使用 have_attributes。
        expect(user).to have_attributes(
          github_username: "Xdite",
          email: "[email protected]",
        )
      end
    end

    context "case insensitive search github username" do
      it "database snake_case, github info lowercase" do
        create(:user, github_username: "Xdite")

        user = User.find_or_create_with(github_username: "xdite")
        user.reload

        expect(user.github_username).to eq "xdite"
      end

      it "database lowercase, github info snake_case" do
        create(:user, github_username: "xdite")

        user = User.find_or_create_with(github_username: "Xdite")
        user.reload

        expect(user.github_username).to eq "Xdite"
      end
    end
  end
end

測試實體方法

class User < ApplicationRecord
  def to_s
    github_username
  end
end

來源:https://git.io/v6J7v

RSpec.describe User do
  describe "#to_s" do
    it "returns GitHub username" do
      user = build(:user)

      user_string = user.to_s

      expect(user_string).to eq user.github_username
    end
  end
end

來源:https://git.io/v6J7J

測試 Callback

盡量避免使用 Callback,用 Callback 時,讓 Callback 呼叫一個方法,測試那個方法的行為即可。

# https://git.io/v6JHp
class User < ApplicationRecord
  before_create :generate_remember_token

  def generate_remember_token
    self.remember_token = SecureRandom.hex(20)
  end
end

# https://git.io/v6JHh
RSpec.describe User do
  describe "#create" do
    it "generates a remember_token" do
      user = build(:user)
      allow(SecureRandom).to receive(:hex) { "remembertoken" }

      user.save

      expect(SecureRandom).to have_received(:hex).with(20)
      expect(user.remember_token).to eq "remembertoken"
    end
  end
end

測 Constant

RSpec.describe User do
  ...

  context "constants" do
    it "A should return sth"
    it "B should return sth"
    ...
  end

  ...
end

可以用 Ruby 的 const_get

describe Bundler::Audit do
  it "should have a VERSION constant" do
    expect(subject.const_get("VERSION")).not_to be_empty
  end
end

範例來源:https://git.io/v6J7X

或是這樣也可以:

describe Bundler::Audit do
  it "should have a VERSION constant" do
    expect(Bundler::Audit::VERSION).to be_present
  end
end

延伸閱讀

Rails 官方關於 Model 測試的文章:Model Testing

results matching ""

    No results matching ""