2013-05-09 80 views
20

我有以下的(简化)Rails的关注:导轨及RSpec的 - 测试关注类的方法

module HasTerms 
    extend ActiveSupport::Concern 

    module ClassMethods 
    def optional_agreement 
     # Attributes 
     #---------------------------------------------------------------------------- 
     attr_accessible :agrees_to_terms 
    end 

    def required_agreement 
     # Attributes 
     #---------------------------------------------------------------------------- 
     attr_accessible :agrees_to_terms 

     # Validations 
     #---------------------------------------------------------------------------- 
     validates :agrees_to_terms, :acceptance => true, :allow_nil => :false, :on => :create 
    end 
    end 
end 

我想不出不过来测试RSpec的这个模块的好方法 - 如果我只是创造一个假类,当我尝试检查验证是否正常工作时,我收到活动记录错误。有其他人遇到过这个问题吗?

回答

39

查看RSpec shared examples

这样,你可以写:

# spec/support/has_terms_tests.rb 
shared_examples "has terms" do 
    # Your tests here 
end 


# spec/wherever/has_terms_spec.rb 
module TestTemps 
    class HasTermsDouble 
    include ActiveModel::Validations 
    include HasTerms 
    end 
end 

describe HasTerms do 

    context "when included in a class" do 
    subject(:with_terms) { TestTemps::HasTermsDouble.new } 

    it_behaves_like "has terms" 
    end 

end 


# spec/model/contract_spec.rb 
describe Contract do 

    it_behaves_like "has terms" 

end 
+0

这显然是最好的答案。在Dummy情况下可以显式地进行测试,并在父类的规范中测试相同的API。这在处理任何“灵活”的API时都有所不同(阅读:method_missing)。只有一些情况只有在“真实”(非虚拟)类中使用时才能想到,并且共享示例在每种必要情况下都会很好地执行代码。 – winfred 2013-07-29 17:36:47

+0

当你的模块添加动态属性时,这会崩溃。假设你的模块允许类方法:'allow_upload:csv',它增加了'csv_file_path'和'csv_file_size'等方法。但是你有另一个模型调用上传的文件':attachment'。现在你的“作为上传”规范将失败,因为一个是添加'csv_file_path',另一个'attachment_file_path'。基于这个原因,我觉得在很多情况下,最好使用一个虚拟类来测试模块的行为,就像@Martijn的回答 – nzifnab 2014-05-07 21:24:00

+1

@nzifnab一样清楚,模块没有添加方法,子类是明确的。这里的共享示例是否合适是针对代码库的判断调用。但是,您仍然可以以这种方式使用它们。有可能将信息传递给他们,就像你在电话中一样:'it_behaves_like'充当上传',:csv' – 2014-05-08 03:36:00

6

您可以通过将测试留在包含此模块的类中来隐式测试模块。或者,您可以在虚拟课堂中添加其他必需的模块。例如,AR模型中的validates方法由ActiveModel::Validations提供。因此,对于你的测试:

class DummyClass 
    include ActiveModel::Validations 
    include HasTerms 
end 

有可能是你需要带的基础上依赖你的隐模块HasTerms在依靠其他模块。

+0

我同意隐含的测试很容易,但我觉得我需要能够也明确地测试事情。当涉及到编写没有任何类使用的类函数时,这尤其重要。 – Bryce 2013-05-09 13:36:00

+0

就像'DummyClass'是你正在寻找的另一种选择。 – rossta 2013-05-09 15:24:49

5

我用这个自己挣扎着编造了以下解决方案,这一点很像rossta的想法,但使用匿名类来代替:

it 'validates terms' do 
    dummy_class = Class.new do 
    include ActiveModel::Validations 
    include HasTerms 

    attr_accessor :agrees_to_terms 

    def self.model_name 
     ActiveModel::Name.new(self, nil, "dummy") 
    end 
    end 

    dummy = dummy_class.new 
    dummy.should_not be_valid 
end 
+0

这是我发现的唯一解决方案。当你使用命名虚拟类时,如果你调用一个修改该类的方法,那么你的规范就会相互渗透,那么下一个规范也会看到这个类的修改。尽管我更喜欢'let(:dummy_class){Class.new(...)}',而不是将它直接插入到'it'块中。 – nzifnab 2014-05-07 21:26:29

2

建立在Aaron K的优秀答案here上,您可以使用一些不错的技巧与RSpec提供的described_class使您的方法无处不在,并使工厂为您工作。这里有一个共享的例子的片断我最近的一个应用程序进行:

shared_examples 'token authenticatable' do 
    describe '.find_by_authentication_token' do 
    context 'valid token' do 
     it 'finds correct user' do 
     class_symbol = described_class.name.underscore 
     item = create(class_symbol, :authentication_token) 
     create(class_symbol, :authentication_token) 

     item_found = described_class.find_by_authentication_token(
      item.authentication_token 
     ) 

     expect(item_found).to eq item 
     end 
    end 

    context 'nil token' do 
     it 'returns nil' do 
     class_symbol = described_class.name.underscore 
     create(class_symbol) 

     item_found = described_class.find_by_authentication_token(nil) 

     expect(item_found).to be_nil 
     end 
    end 
    end 
end 
3

下面是另一个例子(使用Factorygirl的“创造”方法”和shared_examples_for)

关注规范

#spec/support/concerns/commentable_spec 
require 'spec_helper' 
shared_examples_for 'commentable' do 
    let (:model) { create (described_class.to_s.underscore) } 
    let (:user) { create (:user) } 

    it 'has comments' do 
    expect { model.comments }.to_not raise_error 
    end 
    it 'comment method returns Comment object as association' do 
    model.comment(user, "description") 
    expect(model.comments.length).to eq(1) 
    end 
    it 'user can make multiple comments' do 
    model.comment(user, "description") 
    model.comment(user, "description") 
    expect(model.comments.length).to eq(2) 
    end 
end 

commentable关注

module Commentable 
    extend ActiveSupport::Concern 
    included do 
    has_many :comments, as: :commentable 
    end 

    def comment(user, description) 
    Comment.create(commentable_id: self.id, 
        commentable_type: self.class.name, 
        user_id: user.id, 
        description: description 
       ) 
    end 

end 

and restraunt_spec可能看起来像这样(我n OT Rspec的大师,所以不要认为我写的规格的方式是很好的 - 最重要的事情是开头):

require 'rails_helper' 

RSpec.describe Restraunt, type: :model do 
    it_behaves_like 'commentable' 

    describe 'with valid data' do 
    let (:restraunt) { create(:restraunt) } 
    it 'has valid factory' do 
     expect(restraunt).to be_valid 
    end 
    it 'has many comments' do 
     expect { restraunt.comments }.to_not raise_error 
    end 
    end 
    describe 'with invalid data' do 
    it 'is invalid without a name' do 
     restraunt = build(:restraunt, name: nil) 
     restraunt.save 
     expect(restraunt.errors[:name].length).to eq(1) 
    end 
    it 'is invalid without description' do 
     restraunt = build(:restraunt, description: nil) 
     restraunt.save 
     expect(restraunt.errors[:description].length).to eq(1) 
    end 
    it 'is invalid without location' do 
     restraunt = build(:restraunt, location: nil) 
     restraunt.save 
     expect(restraunt.errors[:location].length).to eq(1) 
    end 
    it 'does not allow duplicated name' do 
     restraunt = create(:restraunt, name: 'test_name') 
     restraunt2 = build(:restraunt, name: 'test_name') 
     restraunt2.save 
     expect(restraunt2.errors[:name].length).to eq(1) 
    end 
    end 
end