簡體   English   中英

使用shoulda在Rails模型上重構rspec測試

[英]Using shoulda to refactor rspec tests on Rails models

通過回答關於屬性可訪問性測試的另一個StackOverflow問題 (並認為它們非常棒)來了解shoulda-matchers之后,我決定嘗試重構我在The Rails Tutorial中所做的模型測試,試圖使它們更加簡潔和徹底。 我這樣做歸功於來自模塊的文檔的一些靈感: Shoulda::Matchers::ActiveRecordShoulda::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

關於這些測試的一些具體問題:

  1. 是否值得測試數據庫模式? 上面提到StackOverflow答案中的注釋說“我只測試與行為相關的事情,我不考慮列的存在或索引行為。數據庫列不會消失,除非有人故意刪除它們,但是你我可以通過代碼審查和信任保護“,我同意這一點,但是有沒有任何正當理由可以測試數據庫模式的結構,從而證明了Shoulda::Matchers::ActiveRecord模塊的存在? 也許只是重要的指標值得測試......?
  2. 是否should have_many"associations"下進行should have_many測試,在"model attributes"下替換相應的should respond_to測試? 我不知道是否should have_many測試只是在模型文件中查找相關的has_many聲明,或者實際執行與should respond_to相同的函數。
  3. 您是否有任何其他意見/建議可以使這些測試在內容和結構上更加簡潔/可讀/徹底?

1)Shoulda :: Matchers :: ActiveRecord模塊中的內容比列和索引匹配器要多得多。 我會在附帶的課程中挖掘一下,看看你能找到什么。 這就是have_manybelong_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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM