简体   繁体   中英

Rails & RSpec - Testing Concerns class methods

I have the following (simplified) Rails Concern:

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

I can't figure out a good way to test this module in RSpec however - if I just create a dummy class, I get active record errors when I try to check that the validations are working. Has anyone else faced this problem?

Check out RSpec shared examples .

This way you can write the following:

# 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

You could just test the module implicitly by leaving your tests in the classes that include this module. Alternatively, you can include other requisite modules in your dummy class. For instance, the validates methods in AR models are provided by ActiveModel::Validations . So, for your tests:

class DummyClass
  include ActiveModel::Validations
  include HasTerms
end

There may be other modules you need to bring in based on dependencies you implicitly rely on in your HasTerms module.

I was struggling with this myself and conjured up the following solution, which is much like rossta's idea but uses an anonymous class instead:

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

Here is another example (using Factorygirl's "create" method" and shared_examples_for)

concern spec

#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 concern

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 may look something like this (I'm not Rspec guru so don't think that my way of writing specs is good - the most important thing is at the beginning):

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

Building on Aaron K's excellent answer here , there are some nice tricks you can use with described_class that RSpec provides to make your methods ubiquitous and make factories work for you. Here's a snippet of a shared example I recently made for an application:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM