測試 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
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
測試 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。