RSpec 2 speccing a static ActiveRecord method - ruby

I've got a pretty basic static method on an ActiveRecord model:
#./app/models/comic.rb
class Comic < ActiveRecord::Base
class << self
def furthest
Comic.maximum(:comic_id) || 0
end
end
end
When executing Comic.furthest in the Rails console it returns 0 as I expect. The problem is I am trying to spec this behavior for both the presence and absence of records:
#./spec/app/models/comic_spec.rb
require 'spec_helper'
describe Comic do
describe "#furthest" do
subject { Comic.furthest }
context "when there are no rows in the database" do
it { should == 0 }
end
context "when there are rows in the database" do
before do
Factory.create(:comic, :comic_id => 100)
Factory.create(:comic, :comic_id => 99)
end
it { should == 100 }
end
end
end
All of this appears very basic and straightforward, however my specs are failing with the message:
1) Comic#furthest when there are no rows in the database
Failure/Error: it { should == 0 }
expected: 0
got: nil (using ==)
# ./spec/models/comic_spec.rb:8:in `block (4 levels) in <top (required)>'
Even if I change furthest to simply:
def furthest
0
end
I still get nil (using ==).
The second spec, it { should == 100 } passes with the original Comic.maximum(:comic_id) || 0 definition, as if the Factory.create invocations are required for #furthest to not return nil.
What am I doing wrong?

I am fairly confident this was a problem with me using the p180 release of Ruby 1.9.2 with custom patches to improve require performance. After upgrading to p290 this problem is gone.

Related

Why does changing the order of 'it' and 'subject' in RSpec change my test result?

The class being tested qa.rb contains the code:
class QA
def initialize(bugs: 0)
#bugs = bugs
end
def speak
"Hello!"
end
def happy?
#bugs > 0
end
def debug
#bugs = 0
end
end
The RSpec file qa_spec.rb contains the code:
require 'rspec'
require_relative 'qa'
RSpec.describe QA do
describe '#happy?' do
context 'when bugs are more than 0' do
it 'returns true' do
subject { described_class.new(bugs: 1) }
expect(subject).to be_happy
end
end
end
end
The test fails when I run it, and gives me this error:
PS C:\Users\Jobla\repos\TDD> rspec qa_spec.rb
F
Failures:
1) QA#happy? when bugs are more than 0 returns true
Failure/Error: expect(subject).to be_happy
expected `#<QA:0x2e0d640 #bugs=0>.happy?` to return true, got false
# ./qa_spec.rb:9:in `block (4 levels) in <top (required)>'
Finished in 0.02999 seconds (files took 0.16995 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./qa_spec.rb:7 # QA#happy? when bugs are more than 0 returns true
However, when I edit qa_spec.rb and I swap the it and subject lines, the test suddenly passes:
require 'rspec'
require_relative 'qa'
RSpec.describe QA do
describe '#happy?' do
context 'when bugs are more than 0' do
subject { described_class.new(bugs: 1) } #swapped with line below
it 'returns true' do #swapped with line above
expect(subject).to be_happy
end
end
end
end
Tests pass:
PS C:\Users\Jobla\repos\TDD> rspec qa_spec.rb
.
Finished in 0.01003 seconds (files took 0.17993 seconds to load)
1 example, 0 failures
Please could someone explain why does swapping the it and subject lines change the result of the test?
subject is designed to be set in context or describe block, but not in it.
If you do not set subject before it then subject would be set automatically by calling new without parameters on described_class. bugs will be set to default 0. After that, you call it with a block subject { described_class.new(bugs: 1) } inside it, it's the same as if you call described_class.new { described_class.new(bugs: 1) } because subject inside it is an instance of QA class.

Rspec not finding class methods

I'm writing some tests for my backend jobs and I'm having a weird issue with rspec not finding my methods.
I wrote a simple class & test to illustrate the issue :
app/interactors/tmp_test.rb :
class TmpTest
def call
a = 10
b = 5
b.substract_two
return a + b
end
def substract_two
c = self - 2
return c
end
end
spec/interactors/tmp_test.rb :
require 'rails_helper'
describe TmpTest do
context 'when doing the substraction' do
it 'return the correct number' do
expect(described_class.call).to eq(13)
end
end
end
output:
TmpTest
when doing the substraction
return the correct number (FAILED - 1)
Failures:
1) TmpTest when doing the substraction return the correct number
Failure/Error: expect(described_class.call).to eq(13)
NoMethodError:
undefined method `call' for TmpTest:Class
# ./spec/interactors/tmp_test.rb:6:in `block (3 levels) in <top (required)>'
Finished in 0.00177 seconds (files took 1.93 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/interactors/tmp_test.rb:5 # TmpTest when doing the substraction return the correct number
It's not a class method, it's an instance method. Your test should look like this:
describe TmpTest do
subject(:instance) { described_class.new }
context 'when doing the subtraction' do
it 'returns the correct number' do
expect(instance.call).to eq(13)
end
end
end
This is a complete mess. Corrected version with comments:
class TmpTest
def call
a = 10
b = 5
# b.substract_two # why do you call method of this class on b?!
a + subtract_two(b)
end
def substract_two(from)
from - 2
end
end
Also: don’t use return in the very last line of the method.

rspec stub to allow [hash_key] to be passed

How do you create a rspec method stub to allow a response from a method that takes in the hash key to return its value?
This is the line I want to test
sub_total = menu.menu_items[item] * quantity
and I'm using this line in rspec as my test stub on a double.
allow(menu).to receive(:menu_items[item]).and_return(2.0)
My env is set up with ruby 2.2.0 and spec 3.1.7
However I keep on getting a
NameError: undefined local variable or method `item'
Ruby code
def place_order(item, quantity, menu)
sub_total = menu.menu_items[item] * quantity
#customer_order << [item, quantity, sub_total]
end
Rspec code
let(:menu) { double :menu }
it "should allow 1 order of beer to placed" do
order = Order.new
allow(menu).to receive(:menu_items[item]).and_return(2.0)
order.place_order(:Beer, 1, 2.0)
expect(order.customer_order).to eq [[:Beer, 1, 2.0]]
end
Failures:
1) Order should allow 1 order of beer to placed
Failure/Error: allow(menu).to receive(:menu_items[item]).and_return(2.0)
NameError:
undefined local variable or method `item' for #<RSpec::ExampleGroups::Order:0x007fbb62917ee8 #__memoized=nil>
# ./spec/order_spec.rb:9:in `block (2 levels) in <top (required)>'
I've tried a number of things but nothing has worked
allow(menu).to receive(:menu_items).and_return(2.0)
allow(menu).to receive(:menu_items).with(item).and_return(2.0)
allow(menu).to receive(:menu_items).with("item").and_return(2.0)
allow(menu).to receive(:menu_items).with([item]).and_return(2.0)
I've run my code in irb and I can see it works but I can't find a way to get my class double to recerive the hash key.
you can do this:
allow(menu.menu_items).to receive(:[]).and_return({Beer: 2.0})
You can also pass an specific item if you need:
allow(menu.menu_items).to receive(:[]).with(1).and_return({Beer: 2.0})
The line menu.menu_items[item] is in reality composed by 3 method calls. [] is a call to the method [] on the Hash returned by menu_items.
I assume menu.menu_items returns a Hash and not an Array, given in the spec item is a Symbol.
That means your stub requires a little bit more work.
allow(menu).to receive(:menu_items).and_return({ Beer: 2.0 })
Also note, the error
undefined local variable or method `item'
is because you were using item in the spec, but item is not defined outside your method.
you're going a little too deep with your stub, think of this instead
allow(menu).to receive(:menu_items).and_return({Beer: 2.0})
Thanks to #SimoneCarletti's answer, I was able to easily stub an instance of PublicActivity. I add this answer only as a more brief (re)statement of the OP's problem and the simplicity of the solution.
Code I want to mimic with a stub:
self.entity = activity.parameters['entity_string']
And the salient parts of the test double:
activity = double('activity') # PublicActivity
allow(activity).to receive(:parameters).and_return({'entity_string' => "some entity name"})
Full code:
class ActivityRenderer
attr_accessor :time
attr_accessor :user
attr_accessor :action
attr_accessor :entity
def initialize(activity)
self.entity = activity.parameters['entity_string']
self.time = activity.updated_at
self.user = User.find(activity.owner_id)
self.action = activity.key
end
end
RSpec.describe ActivityRenderer do
let(:user) { ...factory girl stuff... }
let(:now) { Time.zone.now }
before do
Timecop.freeze
end
it 'provides an activity renderer' do
activity = double('activity') # PublicActivity
allow(activity).to receive(:parameters).and_return({'entity_string' => "some entity name"})
allow(activity).to receive(:updated_at).and_return(now)
allow(activity).to receive(:owner_id).and_return(user._id)
allow(activity).to receive(:key).and_return('some activity?')
ar = ActivityRenderer.new(activity)
expect(ar.user).to eql(user)
expect(ar.time).to eql(now)
expect(ar.action).to eql('some activity?')
expect(ar.entity).to eql("some entity name")
end
end

Testing with Rspec codeschool level 3 challenge 5

I have been battling this test way too long and I am not sure where I am stuck. Here is the model I am trying to test:
class Zombie < ActiveRecord::Base
attr_accessible :iq
validates :name, presence: true
def genius?
iq >= 3
end
def self.genius
where("iq >= ?", 3)
end
end
Here is what I am working with to start:
describe Zombie do
context "with high iq" do
let(:zombie) { Zombie.new(iq: 3, name: 'Anna') }
subject { zombie }
it "should be returned with genius" do
Zombie.genius.should include(zombie)
end
it "should have a genius count of 1" do
Zombie.genius.count.should == 1
end
end
end
This is the part of the refactor that is working:
it { should be_genius }
#it "should have a genius count of 1" do
# Zombie.genius.count.should == 1
#end
Here is where I am currently stuck at with the refactor:
describe Zombie do
context "with high iq" do
let!(:zombie) { Zombie.new(iq: 3, name: 'Anna') }
subject { zombie }
it {should include("zombie")}
it { should be_genius }
end
end
According to the examples this should work, but no matter what I try it keeps bombing on the include. I know I am missing something lame here. Thoughts or tips anyone?
Current Error Message:
Failures:
1) Zombie with high iq
Failure/Error: it {should include("zombie")}
NoMethodError:
undefined method `include?' for #<Zombie:0x00000006792380>
# zombie_spec.rb:7:in `block (3 levels) '
Finished in 0.12228 seconds
2 examples, 1 failure
Failed examples:
rspec zombie_spec.rb:7 # Zombie with high iq
You need to add the ! to the let and change new to create in order to save the record.
describe Zombie do
context "with high iq" do
let!(:zombie) { Zombie.create(iq: 3, name: 'Anna') }
subject { zombie }
it "should be returned with genius" do
Zombie.genius.should include(zombie)
end
it "should have a genius count of 1" do
Zombie.genius.count.should == 1
end
end
end
I'm not sure what examples you are referring to that suggest your refactor should work, but the implicit subject zombie used in your first refactored example is an ActiveRecord instance and the include matcher you're using is intended to be used with a string, array or hash as described in https://www.relishapp.com/rspec/rspec-expectations/v/2-0/docs/matchers/include-matcher.
As for your second refactored example, I gather it's working because you've only indicated a problem with the include.

Having 'allocator undefined for Data' when saving with ActiveResource

What I am missing? I am trying to use a rest service for with Active resource, I have the following:
class User < ActiveResource::Base
self.site = "http://localhost:3000/"
self.element_name = "users"
self.format = :json
end
user = User.new(
:name => "Test",
:email => "test.user#domain.com")
p user
if user.save
puts "success: #{user.uuid}"
else
puts "error: #{user.errors.full_messages.to_sentence}"
end
And the following output for the user:
#<User:0x1011a2d20 #prefix_options={}, #attributes={"name"=>"Test", "email"=>"test.user#domain.com"}>
and this error:
/Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1233:in `new': allocator undefined for Data (TypeError)
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1233:in `load'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1219:in `each'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1219:in `load'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1322:in `load_attributes_from_response'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1316:in `create_without_notifications'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1314:in `tap'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1314:in `create_without_notifications'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/observing.rb:11:in `create'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1117:in `save_without_validation'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/validations.rb:87:in `save_without_notifications'
from /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/observing.rb:11:in `save'
from import_rest.rb:22
If I user curl for my rest service it would be like:
curl -v -X POST -H 'Content-Type: application/json' -d '{"name":"test curl", "email":"test#gmail.com"}' http://localhost:3000/users
with the response:
{"email":"test#gmail.com","name":"test curl","admin":false,"uuid":"afb8c98b-562a-4603-bbe4-f8f0816cef0d","creation_limit":5}
There is a built-in type named Data, whose purpose is rather mysterious. You appear to be bumping into it:
$ ruby -e 'Data.new'
-e:1:in `new': allocator undefined for Data (TypeError)
from -e:1
The question is, how did it get there? The last stack frame puts us here. So, it appears Data wandered out of a call to find_or_create_resource_for. The code branch here looks likely:
$ irb
>> class C
>> end
=> nil
>> C.const_get('Data')
=> Data
This leads me to suspect you have an attribute or similar floating around named :data or "data", even though you don't mention one above. Do you? Particularly, it seems we have a JSON response with a sub-hash whose key is "data".
Here's a script that can trigger the error for crafted input, but not from the response you posted:
$ cat ./activeresource-oddity.rb
#!/usr/bin/env ruby
require 'rubygems'
gem 'activeresource', '3.0.10'
require 'active_resource'
class User < ActiveResource::Base
self.site = "http://localhost:3000/"
self.element_name = "users"
self.format = :json
end
USER = User.new :name => "Test", :email => "test.user#domain.com"
def simulate_load_attributes_from_response(response_body)
puts "Loading #{response_body}.."
USER.load User.format.decode(response_body)
end
OK = '{"email":"test#gmail.com","name":"test curl","admin":false,"uuid":"afb8c98b-562a-4603-bbe4-f8f0816cef0d","creation_limit":5}'
BORKED = '{"data":{"email":"test#gmail.com","name":"test curl","admin":false,"uuid":"afb8c98b-562a-4603-bbe4-f8f0816cef0d","creation_limit":5}}'
simulate_load_attributes_from_response OK
simulate_load_attributes_from_response BORKED
produces..
$ ./activeresource-oddity.rb
Loading {"email":"test#gmail.com","name":"test curl","admin":false,"uuid":"afb8c98b-562a-4603-bbe4-f8f0816cef0d","creation_limit":5}..
Loading {"data":{"email":"test#gmail.com","name":"test curl","admin":false,"uuid":"afb8c98b-562a-4603-bbe4-f8f0816cef0d","creation_limit":5}}..
/opt/local/lib/ruby/gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1233:in `new': allocator undefined for Data (TypeError)
from /opt/local/lib/ruby/gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1233:in `load'
from /opt/local/lib/ruby/gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1219:in `each'
from /opt/local/lib/ruby/gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb:1219:in `load'
from ./activeresource-oddity.rb:17:in `simulate_load_attributes_from_response'
from ./activeresource-oddity.rb:24
If I were you, I would open /Library/Ruby/Gems/1.8/gems/activeresource-3.0.10/lib/active_resource/base.rb, find load_attributes_from_response on line 1320 and temporarily change
load(self.class.format.decode(response.body))
to
load(self.class.format.decode(response.body).tap { |decoded| puts "Decoded: #{decoded.inspect}" })
..and reproduce the error again to see what is really coming out of your json decoder.
I just ran into the same error in the latest version of ActiveResource, and I found a solution that does not require monkey-patching the lib: create a Data class in the same namespace as the ActiveResource object. E.g.:
class User < ActiveResource::Base
self.site = "http://localhost:3000/"
self.element_name = "users"
self.format = :json
class Data < ActiveResource::Base; end
end
Fundamentally, the problem has to do with the way ActiveResource chooses the classes for the objects it instantiates from your API response. It will make an instance of something for every hash in your response. For example, it'll want to create User, Data and Pet objects for the following JSON:
{
"name": "Bob",
"email": "bob#example.com",
"data": {"favorite_color": "purple"},
"pets": [{"name": "Puffball", "type": "cat"}]
}
The class lookup mechanism can be found here. Basically, it checks the resource (User) and its ancestors for a constant matching the name of the sub-resource it wants to instantiate (i.e. Data here). The exception is caused by the fact that this lookup finds the top-level Data constant from the Stdlib; you can therefore avoid it by providing a more specific constant in the resource's namespace (User::Data). Making this class inherit from ActiveResource::Base replicates the behaviour you'd get if the constant was not found at all (see here).
Thanks to phs for his analysis - it got me pointed in the right direction.
I had no choice but to hack into ActiveResource to fix this problem because an external service over which I have no control had published an API where all attributes of the response were tucked away inside a top-level :data attribute.
Here's the hack I ended up putting in config/initializers/active_resource.rb to get this working for me using active resource 3.2.8:
class ActiveResource::Base
def load(attributes, remove_root = false)
raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
#prefix_options, attributes = split_options(attributes)
if attributes.keys.size == 1
remove_root = self.class.element_name == attributes.keys.first.to_s
end
# THIS IS THE PATCH
attributes = ActiveResource::Formats.remove_root(attributes) if remove_root
if data = attributes.delete(:data)
attributes.merge!(data)
end
# END PATCH
attributes.each do |key, value|
#attributes[key.to_s] =
case value
when Array
resource = nil
value.map do |attrs|
if attrs.is_a?(Hash)
resource ||= find_or_create_resource_for_collection(key)
resource.new(attrs)
else
attrs.duplicable? ? attrs.dup : attrs
end
end
when Hash
resource = find_or_create_resource_for(key)
resource.new(value)
else
value.duplicable? ? value.dup : value
end
end
self
end
class << self
def find_every(options)
begin
case from = options[:from]
when Symbol
instantiate_collection(get(from, options[:params]))
when String
path = "#{from}#{query_string(options[:params])}"
instantiate_collection(format.decode(connection.get(path, headers).body) || [])
else
prefix_options, query_options = split_options(options[:params])
path = collection_path(prefix_options, query_options)
# THIS IS THE PATCH
body = (format.decode(connection.get(path, headers).body) || [])
body = body['data'] if body['data']
instantiate_collection( body, prefix_options )
# END PATCH
end
rescue ActiveResource::ResourceNotFound
# Swallowing ResourceNotFound exceptions and return nil - as per
# ActiveRecord.
nil
end
end
end
end
I solved this using a monkey-patch approach, that changes "data" to "xdata" before running find_or_create_resource_for (the offending method). This way when the find_or_create_resource_for method runs it won't search for the Data class (which would crash). It searches for the Xdata class instead, which hopefully doesn't exist, and will be created dynamically by the method. This will be a a proper class subclassed from ActiveResource.
Just add a file containig this inside config/initializers
module ActiveResource
class Base
alias_method :_find_or_create_resource_for, :find_or_create_resource_for
def find_or_create_resource_for(name)
name = "xdata" if name.to_s.downcase == "data"
_find_or_create_resource_for(name)
end
end
end

Resources