[英]Using shoulda to refactor rspec tests on Rails models
通過回答關於屬性可訪問性測試的另一個StackOverflow問題 (並認為它們非常棒)來了解shoulda-matchers之后,我決定嘗試重構我在The Rails Tutorial中所做的模型測試,試圖使它們更加簡潔和徹底。 我這樣做歸功於來自模塊的文檔的一些靈感: Shoulda::Matchers::ActiveRecord
和Shoulda::Matchers::ActiveModel
,以及這個StackOverflow關於結構化應該在模型中進行測試的答案 。 但是,還有一些我不確定的事情,我想知道如何使這些測試更好。
我將使用Rails教程中的用戶規范作為我的示例,因為它是最詳細的,並涵蓋了許多可以改進的領域。 以下代碼示例已從原始user_spec.rb更改,並將代碼替換為describe "micropost associations"
行。 針對user.rb模型的規范測試及其工廠在factories.rb中定義。
規格/型號/ user_spec.rb
# == Schema Information
#
# Table name: users
#
# id :integer not null, primary key
# name :string(255)
# email :string(255)
# created_at :datetime not null
# updated_at :datetime not null
# password_digest :string(255)
# remember_token :string(255)
# admin :boolean default(FALSE)
#
# Indexes
#
# index_users_on_email (email) UNIQUE
# index_users_on_remember_token (remember_token)
#
require 'spec_helper'
describe User do
let(:user) { FactoryGirl.create(:user) }
subject { user }
describe "database schema" do
it { should have_db_column(:id).of_type(:integer)
.with_options(null: false) }
it { should have_db_column(:name).of_type(:string) }
it { should have_db_column(:email).of_type(:string) }
it { should have_db_column(:created_at).of_type(:datetime)
.with_options(null: false) }
it { should have_db_column(:updated_at).of_type(:datetime)
.with_options(null: false) }
it { should have_db_column(:password_digest).of_type(:string) }
it { should have_db_column(:remember_token).of_type(:string) }
it { should have_db_column(:admin).of_type(:boolean)
.with_options(default: false) }
it { should have_db_index(:email).unique(true) }
it { should have_db_index(:remember_token) }
end
describe "associations" do
it { should have_many(:microposts).dependent(:destroy) }
it { should have_many(:relationships).dependent(:destroy) }
it { should have_many(:followed_users).through(:relationships) }
it { should have_many(:reverse_relationships).class_name("Relationship")
.dependent(:destroy) }
it { should have_many(:followers).through(:reverse_relationships) }
end
describe "model attributes" do
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should respond_to(:password_digest) }
it { should respond_to(:remember_token) }
it { should respond_to(:admin) }
it { should respond_to(:microposts) }
it { should respond_to(:relationships) }
it { should respond_to(:followed_users) }
it { should respond_to(:reverse_relationships) }
it { should respond_to(:followers) }
end
describe "virtual attributes and methods from has_secure_password" do
it { should respond_to(:password) }
it { should respond_to(:password_confirmation) }
it { should respond_to(:authenticate) }
end
describe "accessible attributes" do
it { should_not allow_mass_assignment_of(:password_digest) }
it { should_not allow_mass_assignment_of(:remember_token) }
it { should_not allow_mass_assignment_of(:admin) }
end
describe "instance methods" do
it { should respond_to(:feed) }
it { should respond_to(:following?) }
it { should respond_to(:follow!) }
it { should respond_to(:unfollow!) }
end
describe "initial state" do
it { should be_valid }
it { should_not be_admin }
its(:remember_token) { should_not be_blank }
its(:email) { should_not =~ /\p{Upper}/ }
end
describe "validations" do
context "for name" do
it { should validate_presence_of(:name) }
it { should_not allow_value(" ").for(:name) }
it { should ensure_length_of(:name).is_at_most(50) }
end
context "for email" do
it { should validate_presence_of(:email) }
it { should_not allow_value(" ").for(:email) }
it { should validate_uniqueness_of(:email).case_insensitive }
context "when email format is invalid" do
addresses = %w[user@foo,com user_at_foo.org example.user@foo.]
addresses.each do |invalid_address|
it { should_not allow_value(invalid_address).for(:email) }
end
end
context "when email format is valid" do
addresses = %w[user@foo.COM A_US-ER@f.b.org frst.lst@foo.jp a+b@baz.cn]
addresses.each do |valid_address|
it { should allow_value(valid_address).for(:email) }
end
end
end
context "for password" do
it { should ensure_length_of(:password).is_at_least(6) }
it { should_not allow_value(" ").for(:password) }
context "when password doesn't match confirmation" do
it { should_not allow_value("mismatch").for(:password) }
end
end
context "for password_confirmation" do
it { should validate_presence_of(:password_confirmation) }
end
end
# ...
end
關於這些測試的一些具體問題:
Shoulda::Matchers::ActiveRecord
模塊的存在? 也許只是重要的指標值得測試......? should have_many
在"associations"
下進行should have_many
測試,在"model attributes"
下替換相應的should respond_to
測試? 我不知道是否should have_many
測試只是在模型文件中查找相關的has_many
聲明,或者實際執行與should respond_to
相同的函數。 1)Shoulda :: Matchers :: ActiveRecord模塊中的內容比列和索引匹配器要多得多。 我會在附帶的課程中挖掘一下,看看你能找到什么。 這就是have_many
, belong_to
等來自的地方。 盡管如此,我認為大部分內容都沒什么價值。
2)是的,像have_many
這樣的宏測試比模型是否響應方法要多得多。 從源代碼中 ,您可以准確地看到它正在測試的內容:
def matches?(subject)
@subject = subject
association_exists? &&
macro_correct? &&
foreign_key_exists? &&
through_association_valid? &&
dependent_correct? &&
class_name_correct? &&
order_correct? &&
conditions_correct? &&
join_table_exists? &&
validate_correct?
end
3)使測試更具可讀性和/或簡潔性絕對是一個主觀的問題。 根據他們的背景和經驗,每個人都會給你一個不同的答案。 我個人會擺脫所有的respond_to
測試,並用有價值的測試替換它們。 當有人查看您的測試時,他們應該能夠理解該類的公共API。 當我看到你的對象響應“跟隨?”之類的東西時,我可以做出假設,但不知道它意味着什么。 是否需要爭論? 它返回一個布爾值嗎? 對象是跟隨某個東西還是跟隨對象的東西?
您的問題涉及幾點,我想解決其中兩個問題:
答案是主觀的,所以我會給你個人看法。
1)那樣測試ActiveRecord?
我的回答是肯定的。 您可以使用真實數據編寫復雜的測試,但如果您基本上信任ActiveRecord,您可以這樣做,如果您開始執行tdd,首先使用這些測試,他們可以在此過程中提供幫助。
2)完全寫出模型測試?
我的回答是肯定的。 我所做的是將控制器和請求規范集中在快樂路徑上,然后對於需要驗證等的情況,我為它們編寫單元模型測試。 事實證明,這對我來說是一個很好的責任分工。
我認為應該從規范的角度來看待這一切。
如果您有一個組件測試級別規范,該規范涵蓋給定模型的必要數據庫列,則應該,否則不應該。
如果沒有覆蓋,但作為一個負責任的開發人員,你覺得重要(你的sw和它的質量特性更好),你必須安排在規范中包含這些信息,然后你可以把這些測試放在測試套件中。
較低的測試級別要求主要來自組織內部(內部文檔),客戶主要僅提供客戶需求規范(假設這是測試V模型的最高級別)。 隨着您的組織開始設計,sw會逐步創建較低的測試級別規范。
對於“我們真的需要這個”問題:它取決於許多因素:應用程序復雜性,安全性是否關鍵,遵循的標准,合同/法律/工業法規等。
通常我會說,對於正確的理想應用程序,負責單元測試的要求應該編寫單元級規范,測試人員應該根據此規范實現測試。
對於“have_many和respond_to”,我恐怕沒有背景信息如何實現,所以無法回答。
我在為數據庫列的存在編寫測試時發現了一些價值。 原因如下:
1)寫它們讓我保持TDD的節奏。
2)遷移通常非常棒,直到它們不是。 我知道你不應該編輯現有的遷移,但是當我自己正在處理某些事情時,我有時會這樣做。 如果其他人正在使用相同的應用程序並更改現有的遷移而不是編寫新的遷移,那么這些測試很快就能解決問題。
如果你陷入了太多的列名和類型,你可以做這樣的事情來節省自己輸入:
describe User do
describe 'database' do
describe 'columns' do
%w[reset_password_sent_at remember_created_at current_sign_in_at
last_sign_in_at confirmed_at confirmation_sent_at
created_at updated_at
].each do |column|
it { should have_db_column(column.to_sym).of_type(:datetime) }
end
end
describe 'indexes' do
%w[confirmation_token email reset_password_token
].each do |index|
it { should have_db_index(index.to_sym).unique(true)}
end
end
end
end
希望有所幫助。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.