I have a Lesson model which is assosiated with Permission model:
app/models/lesson.rb:
class Lesson < ActiveRecord::Base
has_many :permissions, :class_name => 'Permission', as: :permissible, dependent: :destroy
...
def create_permissions
Permission::DEFAULTS[:lesson].each do |action, value|
..
end
end
app/models/permission.rb:
class Permission < ActiveRecord::Base
DEFAULTS = {
lesson: {some_more_action: 15}
}
belongs_to :permissible, polymorphic: true
end
I used RSpec stub_const method to stub a nested defined constant:
spec/models/lesson_spec.rb:
require 'rails_helper'
RSpec.describe Lesson, :type => :model do
describe "#create_permissions" do
let!(:lesson) { FactoryGirl.build_stubbed :lesson }
before(:each) do
stub_const('Permission::DEFAULTS', {lesson: {some_action: 5}})
end
it 'should create permissions' do
lesson.create_permissions
permission = lesson.permissions.first
...
end
end
end
But the spec fails with error:
Failure/Error: permission = lesson.permissions.first
NoMethodError:
undefined method `relation_delegate_class' for Permission:Module
# /home/install/.rvm/gems/ruby-2.1.4/gems/activerecord-4.2.3/lib/active_record/relation/delegation.rb:112:in `relation_class_for'
# /home/install/.rvm/gems/ruby-2.1.4/gems/activerecord-4.2.3/lib/active_record/relation/delegation.rb:106:in `create'
# /home/install/.rvm/gems/ruby-2.1.4/gems/activerecord-4.2.3/lib/active_record/associations/collection_association.rb:41:in `reader'
# /home/install/.rvm/gems/ruby-2.1.4/gems/activerecord-4.2.3/lib/active_record/associations/builder/association.rb:115:in `permissions'
# ./spec/models/lesson_spec.rb:285:in `block (3 levels) in <top (required)>'
It looks as if permissions are no longer rails assosiatiated with lesson model. Any ideas on how to get this round.
Gem versions: rspec-3.3.0, rspec-rails-3.3.3, rails-4.2.3.
Take a closer look to this error message:
NoMethodError:
undefined method `relation_delegate_class' for Permission:Module
Permission is described as a Module here, not as a Class. I think this happens because at the moment of stubbing nested const, Permission class has not been loaded yet, and RSpec has to stub it too via Module.
Try this as a workaround:
before(:each) do
Permission # load real ActiveRecord Permission class
stub_const('Permission::DEFAULTS', {lesson: {some_action: 5}})
end
EDIT: As a side note, I don't think that exposing nested constants to other classes is a good idea, you have little control over the constant (just changing a name or value) and you can't wrap any behavior like you do it with a method. I recommend to change public Permission's API to a using a method, it will also be easier to stub:
class Permission < ActiveRecord::Base
DEFAULTS = {
lesson: {some_more_action: 15}
}
def self.defaults
DEFAULTS
end
end
And in your spec:
before(:each) do
allow(Permission).to receive(:defaults).and_return(lesson: {some_action: 5})
end
The only option that I came upon was just to set constant explicitly in an example:
before(:each) do
Permission::DEFAULTS = {lesson: {some_action: 5}}
end
But I don't feel it's a good idea. It rises a couple of warnings also:
spec/models/lesson_spec.rb:279: warning: already initialized constant Permission::DEFAULTS
app/models/permission.rb:2: warning: previous definition of DEFAULTS was here
Related
I don't understand the following infinite loop involving delegate and alias
class Company
field :name
end
class Employee < Professional
include CompanyMember
end
class Professional
include UserProfile
end
module CompanyMember
belongs_to :company
delegate :name, to: :company, prefix: true
alias :organization_name :company_name
end
module UserProfile
def to_s
out = "#{name} "
out += "(#{organization_name})" if respond_to?(:organization_name)
end
def inspect
to_s + super
end
end
I have an Employee with a missing company, and I have the following infinite loop
app/models/concerns/user_profile.rb:94:in `inspect'
app/models/concerns/company_member.rb:8:in `rescue in company_name'
app/models/concerns/company_member.rb:8:in `company_name'
app/models/concerns/user_profile.rb:89:in `to_s'
app/models/concerns/user_profile.rb:94:in `inspect'
app/models/concerns/company_member.rb:8:in `rescue in company_name'
app/models/concerns/company_member.rb:8:in `company_name'
app/models/concerns/user_profile.rb:89:in `to_s'
The problem is in your override of inspect. When you attempt to call a delegated name on a missing company, NoMethodError is raised. Delegated method then tries to rescue it and show you helpful error message.
exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
You see, it calls inspect to get printable version of your object. Unfortunately, it calls .to_s, which is where infinite recursion begins.
MyClass.inspect return incorrect class when I run whole test suite.
Problem:
I have User::CreditCard and ActiveMerchant::Billing::CreditCard classes in project. Last from activemerchant gem.
When I run single spec(rspec spec/models/user/credit_card_spec.rb) then it works correctly.
When I run whole suite(rspec spec) then spec fails with undefined method..., it doesn't matter. The problem is that in this case, my CreditCard class is not mine!!!
When I run single spec and do puts User::CreditCard.inpsect(or just p User::CreditCard, or in pry just User::CreditCard) then it returns User::CreditCard as expected.
When I run whole suite and do p User::CreditCard inside spec then it returns ActiveMerchant::Billing::CreditCard.
Background:
If you don't want to read "background" then be sure that there are NOTE in the end
I'm working with legacy code. So I don't fully know all parts of the image.
I want to create Value Object for credit card in my User. So I've create new tableless model(note the path and class name):
#app/models/user/credit_card.rb
class User::CreditCard
include ActiveModel::Model
delegate :card_number, :card_expiration, :card_type, to: :subscription
def initialize(subscription)
#subscription = subscription || Subscription.new
end
private
attr_reader :subscription
end
Of course I have User model:
#app/models/user.rb
class User
...
has_one :subscription
...
def credit_card
#credit_card ||= User::CreditCard.new(subscription)
end
end
My specs for user/credit_card:
#spec/models/user/credit_card_spec.rb
require 'spec_helper'
# require 'user/credit_card' # if I include this then it works correct
RSpec.describe User::CreditCard, type: :model do
let(:subscription) { build :subscription }
let(:credit_card) do
p User::CreditCard # this result depends on whole/not whole suite run...
# rspec spec => ActiveMerchant::Billing::CreditCard
# rspec spec/models/user => User::CreditCard
User::CreditCard.new(subscription)
end
it 'should delegate alowed messages to user subscription' do
%w[card_number card_expiration card_type].each do |attr|
expect(credit_card.public_send(attr)).to eql subscription.public_send(attr)
end
end
it 'disallow another methods' do
expect { credit_card.unexisted_method }.to raise_error(NoMethodError)
end
end
NOTE:
in spec I can require 'user/credit_card' and then it will work. But why it does not work without it?
Can it be a problem in another places? For example in controllers or somewhere else?
This is a glitch of rails autoloading + ruby constant resolution.
class C; end
CONST = 42
C::CONST
#⇒ (pry):3: warning: toplevel constant CONST referenced by C::CONST
#⇒ 42
Surprisingly enough, CONST was resolved. That is because of Ruby constant resolution algorithm.
One has two options to fix the problem: either to give a different name to the class User::CreditCard or to make sure it’s loaded. Otherwise Rails finds the constant CreditCard in ActiveMerchant::Billing namespace and is happy with using it.
I did everything pretty much as described here: question
But I keep getting error:
NoMethodError: undefined method `parent_model' for Stream (call 'Stream.connection' to establish a connection):Class
In model/concerns faculty_block.rb
module FacultyBlock
extend ActiveSupport::Concern
included do
def find_faculty
resource = self
until resource.respond_to?(:faculty)
resource = resource.parent
end
resource.faculty
end
def parent
self.send(self.class.parent)
end
end
module ClassMethods
def parent_model(model)
##parent = model
end
end
end
[Program, Stream, Course, Department, Teacher].each do |model|
model.send(:include, FacultyBlock)
model.send(:extend, FacultyBlock::ClassMethods) # I added this just to try
end
In initializers:
require "faculty_block"
method call:
class Stream < ActiveRecord::Base
parent_model :program
end
It seems that the Stream is loaded before loading concern, make sure that you have applied the concerns inside the class definition. When rails loader matches class name for Stream constant, it autoloads it before the finishing evaliation of the faculty_block, so replace constants in it with symbols:
[:Program, :Stream, :Course, :Department, :Teacher].each do |sym|
model = sym.to_s.constantize
model.send(:include, FacultyBlock)
model.send(:extend, FacultyBlock::ClassMethods) # I added this just to try
end
I'm performing the simplest test on the following class (inside model's folder):
class Offer
attr_accessor :title, :payout, :thumbnail
def initialize(title, payout, thumbnail)
#title = title
#payout = payout
#thumbnail = thumbnail
end
end
The thing is there's no 'offers' db table. The objects created out of this class are never saved in a database.
Then i perform the tests using rspec:
describe Offer do
it "has a valid factory" do
expect(FactoryGirl.create(:offer)).to be_valid
end
...
end
and FactoryGirl:
FactoryGirl.define do
factory :offer do
skip_create
title { Faker::Name.name }
payout { Faker::Number.number(2) }
thumbnail { Faker::Internet.url }
initialize_with { new(title, payout, thumbnail)}
end
end
And i get the following error:
> undefined method `valid?' for #<Offer:0x00000002b78958>
Because your Offer class is not inheriting from ActiveRecord::Base, you're not getting any of the stuff that comes along with it (such as validations). valid? is a method provided through ActiveRecord's modules, not by Ruby directly, so it won't be available on a basic Ruby class.
If all you care about is validations, then you can include the ActiveModel::Validations module in your class and it will give you valid? as well as validates_presence_of, etc.:
class Offer
include ActiveModel::Validations
...
end
You can also just include ActiveModel to get a couple other things such as ActiveRecord's naming and conversion benefits (as well as validation).
I want to test an inclusion of a module into a class. I am trying define a new class in RSpec:
describe Statusable do
let(:test_class) do
class ModelIncludingStatusable < ActiveRecord::Base
include Statusable
statuses published: "опубликовано", draft: "черновик"
end
end
describe '#statuses' do
it 'sets STATUSES for a model' do
test_class::STATUSES.should == ["опубликовано", "черновик"]
end
end
end
And I get an error:
TypeError:
[ActiveModel::Validations::InclusionValidator] is not a class/module
This is probably because in Statusable I have:
validates_inclusion_of :status, :in => statuses,
:message => "{{value}} должен быть одним из: #{statuses.join ','}"
But if I comment it out, I get:
TypeError:
["опубликовано", "черновик"] is not a class/module
Maybe new class definition isn't the best option, what do I do then? And even if it's not, how can I define a class in RSpec? And how do I fix this error?
Do not define new constant in tests otherwise it will pollute other tests. Instead, use stub_const.
Also, for this is an unit test of Statusable module. If ActiveRecord model is not a necessity, better not to use it.
You can also use class_eval to avoid not opening this class(no matter fake or not) actually
describe Statusable do
before do
stub_const 'Foo', Class.new
Foo.class_eval{ include Statusable }
Foo.class_eval{ statuses published: "foo", draft: "bar"}
end
context '#statuses' do
it 'sets STATUSES for a model' do
FOO::STATUSES.should == ["foo", "bar"]
end
end
end
Though I copied your assertion, I would suggest not to insert a constant say STATUS into the class/module(Foo) who includes this module. Instead, a class method would be better
expect(Foo.status).to eq(["foo", "bar"])
It fails because class definition does not return itself.
$ irb
> class Foo; 1 end
=> 1
you need to do like this:
let(:test_class) do
class ModelIncludingStatusable < ActiveRecord::Base
include Statusable
statuses published: "опубликовано", draft: "черновик"
end
ModelIncludingStatusable # return the class
end
It works but unfortunately, ModelIncludingStatusable will be defined on top-level because of ruby rule.
To capsulize your class, you should do like this:
class self::ModelIncludingStatusable < ActiveRecord::Base
include Statusable
statuses published: "опубликовано", draft: "черновик"
end
let(:test_class) do
self.class::ModelIncludingStatusable # return the class
end
It works perfectly :)
When you call let this define a memoized helper method. You can't class definition in method body.
Another option which I frequently use is to put the entire test in it's own module, e.g.
module Mapping::ModelSpec
module Human
Person = Struct.new(:name, :age, :posessions)
Possession = Struct.new(:name, :value)
end
RSpec.describe Mapping::Model do
it 'can map with base class' do
person = Human::Person.new('Bob Jones', 200, [])
...
end
end
end
While this is a bit cumbersome, it avoids polluting the global namespace, is only slightly more syntax, and is generally easy to understand. Personally, I'd like a better option.. but I'm not sure what that would be.