rspec model testing; method doesn't work - ruby-on-rails

The method itself works as expected, but when I try to test it in the model spec the create part fails. The find part works well. What did I miss?
conversation.rb
scope :between, -> (sender_id, recipient_id) do
where("(conversations.sender_id = ? AND conversations.recipient_id = ?) OR (conversations.sender_id = ? AND conversations.recipient_id = ?)", sender_id, recipient_id, recipient_id, sender_id)
end
def self.create_or_find_conversation(task_assigner_id, task_executor_id)
Conversation.between(task_assigner_id, task_executor_id).first_or_create do |conversation|
conversation.sender_id = task_assigner_id
conversation.recipient_id = task_executor_id
end
end
conversation_spec.rb
describe "class methods" do
let(:sender) { create(:user) }
let(:recipient) { create(:user) }
let(:other_recipient) { create(:user) }
let!(:conversation) { create(:conversation, sender: sender, recipient: recipient) }
context "create_of_find_conversation" do
#this one throws Failure/Error: expect{conv}.to change{Conversation.count}.by(1)
#expected result to have changed by 1, but was changed by 0
it "creates conversation" do
conv = Conversation.create_or_find_conversation(sender, other_recipient)
expect{conv}.to change{Conversation.count}.by(1)
end
#this one is working as expected
it "finds conversation" do
conv = Conversation.create_or_find_conversation(sender, recipient)
expect(conv).to eq(conversation)
end
end
end

I think these code:
it "creates conversation" do
conv = Conversation.create_or_find_conversation(sender, other_recipient)
expect{conv}.to change{Conversation.count}.by(1)
end
should be changed to:
it "creates conversation" do
expect{
Conversation.create_or_find_conversation(sender.id, other_recipient.id)
}.to change{Conversation.count}.by(1)
end
because it was't the value changed the count, but the process.

Regular variables in Ruby are not lazy loading - the right side of the assignment is processed when the variable is assigned.
def do_something(val)
puts "do_something called"
val
end
a = do_something(hello_world)
puts a
# do_something called
# hello world
You would either need to change expection so that the action is called inside the block passed to expect:
it "creates conversation" do
expect do
Conversation.create_or_find_conversation(sender, other_recipient)
end.to change{Conversation.count}.by(1)
end
Or use RSpec's let to create a lazy loading variable:
let(:conv) { Conversation.create_or_find_conversation(sender, other_recipient) }
it "creates conversation" do
expect { conv }.to change{Conversation.count}.by(1)
end
But that does not fix the underlying problem that you are modeling the domain wrong. In a conversation both the sides take turns speaking - so using a recipient_id and sender_id is just plain wrong. Rather its messages that have a sender and receiver.
You can change them to be called whatever you want but it is much simpler to use a proper many to many relation so that you don't need a complex AND OR query.
class User < ActiveRecord::Base
has_many :user_conversations
has_many :conversations, through: user_conversations
end
class Conversation < ActiveRecord::Base
has_many :user_conversations
has_many :users, through: user_conversations
end
# the m2m join model.
class UserCoversation < ActiveRecord::Base
belongs_to :user
belongs_to :conversation
end
You can then simply query:
Conversation.where(users: [a, b])

Related

Test Active Record Callback without hitting database

class Book < ActiveRecord::Base
belongs_to :author
end
class Author < ActiveRecord::Base
belongs_to :publisher
has_many :books
end
class Publisher < ActiveRecord::Base
before_destroy :remove_empty_author
has_many :authors, dependent: destroy_all
def remove_empty_author
books_present = authors.map(&:book).all? { |book| book.present? }
authors.destroy_all unless books_present
end
end
class PublishersController < ApplicationController
def destroy
#publisher = Publisher.find(params[:id].to_i)
#publisher.destroy # for simplicity I only show the destroy call
end
end
Scenario
This is so close to working!
I have a controller test.
I want to test the following without creating a database entry or querying the database:
1. The remove_empty_author call back is called & if I delete it & the before_destroy the test complains.
2. Destroy is called on author.
Note:
I do not want to test the details of the remove_empty_author method. This will be tested in a model unit test delete a publisher and all of the authors only if the author has no books.
What I have tried
describe "#delete," do
context "Publisher has no authors," do
before do
author = mock_model("Author", book: [])
#publisher = Publisher.new(id: 2, authors: [author, author])
current_publisher = mock_model("Publisher", id: 1)
allow(Publisher).to receive(:find).and_return(#publisher)
allow(controller).to receive(:current_publisher).and_return(current_publisher)
end
it "Controller calls destroy on Publisher" do
expect_any_instance_of(Publisher).to receive(:destroy)
end
it "Publisher calls remove_empty_author" do
expect_any_instance_of(Author).to receive(:destroy)
#publisher.run_callbacks(:destroy) do
false # Prevent active record from proceeding with destroy
end
end
end
end
Things that are working
Controller calls destroy on publisher test works.
The test successfully enters the remove_empty_author call back where
books_present = false,
authors = [(Double "Author_1001"), (Double "Author_1001")] authors.destroy_all = []
Error I am trying to overcome
Failure/Error:
DEFAULT_FAILURE_NOTIFIER = lambda { |failure, _opts| raise failure }
Exactly one instance should have received the following message(s) but didn't: destroy
What is not working
expect_any_instance_of(Author).to receive(:destroy)

rspec testing has_many :through and after_save

I have an (I think) relatively straightforward has_many :through relationship with a join table:
class User < ActiveRecord::Base
has_many :user_following_thing_relationships
has_many :things, :through => :user_following_thing_relationships
end
class Thing < ActiveRecord::Base
has_many :user_following_thing_relationships
has_many :followers, :through => :user_following_thing_relationships, :source => :user
end
class UserFollowingThingRelationship < ActiveRecord::Base
belongs_to :thing
belongs_to :user
end
And these rspec tests (I know these are not necessarily good tests, these are just to illustrate what's happening):
describe Thing do
before(:each) do
#user = User.create!(:name => "Fred")
#thing = Thing.create!(:name => "Foo")
#user.things << #thing
end
it "should have created a relationship" do
UserFollowingThingRelationship.first.user.should == #user
UserFollowingThingRelationship.first.thing.should == #thing
end
it "should have followers" do
#thing.followers.should == [#user]
end
end
This works fine UNTIL I add an after_save to the Thing model that references its followers. That is, if I do
class Thing < ActiveRecord::Base
after_save :do_stuff
has_many :user_following_thing_relationships
has_many :followers, :through => :user_following_thing_relationships, :source => :user
def do_stuff
followers.each { |f| puts "I'm followed by #{f.name}" }
end
end
Then the second test fails - i.e., the relationship is still added to the join table, but #thing.followers returns an empty array. Furthermore, that part of the callback never gets called (as if followers is empty within the model). If I add a puts "HI" in the callback before the followers.each line, the "HI" shows up on stdout, so I know the callback is being called. If I comment out the followers.each line, then the tests pass again.
If I do this all through the console, it works fine. I.e., I can do
>> t = Thing.create!(:name => "Foo")
>> t.followers # []
>> u = User.create!(:name => "Bar")
>> u.things << t
>> t.followers # [u]
>> t.save # just to be super duper sure that the callback is triggered
>> t.followers # still [u]
Why is this failing in rspec? Am I doing something horribly wrong?
Update
Everything works if I manually define Thing#followers as
def followers
user_following_thing_relationships.all.map{ |r| r.user }
end
This leads me to believe that perhaps I am defining my has_many :through with :source incorrectly?
Update
I've created a minimal example project and put it on github: https://github.com/dantswain/RspecHasMany
Another Update
Thanks a ton to #PeterNixey and #kikuchiyo for their suggestions below. The final answer turned out to be a combination of both answers and I wish I could split credit between them. I've updated the github project with what I think is the cleanest solution and pushed the changes: https://github.com/dantswain/RspecHasMany
I would still love it if someone could give me a really solid explanation of what is going on here. The most troubling bit for me is why, in the initial problem statement, everything (except the operation of the callback itself) would work if I commented out the reference to followers.
I've had similar problems in the past that have been resolved by reloading the association (rather than the parent object).
Does it work if you reload thing.followers in the RSpec?
it "should have followers" do
#thing.followers.reload
#thing.followers.should == [#user]
end
EDIT
If (as you mention) you're having problems with the callbacks not getting fired then you could do this reloading in the object itself:
class Thing < ActiveRecord::Base
after_save { followers.reload}
after_save :do_stuff
...
end
or
class Thing < ActiveRecord::Base
...
def do_stuff
followers.reload
...
end
end
I don't know why RSpec has issues with not reloading associations but I've hit the same types of problems myself
Edit 2
Although #dantswain confirmed that the followers.reload helped alleviate some of the problems it still didn't fix all of them.
To do that, the solution needed a fix from #kikuchiyo which required calling save after doing the callbacks in Thing:
describe Thing do
before :each do
...
#user.things << #thing
#thing.run_callbacks(:save)
end
...
end
Final suggestion
I believe this is happening because of the use of << on a has_many_through operation. I don't see that the << should in fact trigger your after_save event at all:
Your current code is this:
describe Thing do
before(:each) do
#user = User.create!(:name => "Fred")
#thing = Thing.create!(:name => "Foo")
#user.things << #thing
end
end
class Thing < ActiveRecord::Base
after_save :do_stuff
...
def do_stuff
followers.each { |f| puts "I'm followed by #{f.name}" }
end
end
and the problem is that the do_stuff is not getting called. I think this is the correct behaviour though.
Let's go through the RSpec:
describe Thing do
before(:each) do
#user = User.create!(:name => "Fred")
# user is created and saved
#thing = Thing.create!(:name => "Foo")
# thing is created and saved
#user.things << #thing
# user_thing_relationship is created and saved
# no call is made to #user.save since nothing is updated on the user
end
end
The problem is that the third step does not actually require the thing object to be resaved - its simply creating an entry in the join table.
If you'd like to make sure that the #user does call save you could probably get the effect you want like this:
describe Thing do
before(:each) do
#thing = Thing.create!(:name => "Foo")
# thing is created and saved
#user = User.create!(:name => "Fred")
# user is created BUT NOT SAVED
#user.things << #thing
# user_thing_relationship is created and saved
# #user.save is also called as part of the addition
end
end
You may also find that the after_save callback is in fact on the wrong object and that you'd prefer to have it on the relationship object instead. Finally, if the callback really does belong on the user and you do need it to fire after creating the relationship you could use touch to update the user when a new relationship is created.
UPDATED ANSWER **
This passes rspec, without stubbing, running callbacks for save (after_save callback included ), and checks that #thing.followers is not empty before trying to access its elements. (;
describe Thing do
before :each do
#user = User.create(:name => "Fred");
#thing = Thing.new(:name => 'Foo')
#user.things << #thing
#thing.run_callbacks(:save)
end
it "should have created a relationship" do
#thing.followers.should == [#user]
puts #thing.followers.inspect
end
end
class Thing < ActiveRecord::Base
after_save :some_function
has_many :user_following_thing_relationships
has_many :followers, :through => :user_following_thing_relationships, :source => :user
def some_function
the_followers = followers
unless the_followers.empty?
puts "accessing followers here: the_followers = #{the_followers.inspect}..."
end
end
end
ORIGINAL ANSWER **
I was able to get things to work with the after_save callback, so long as I did not reference followers within the body / block of do_stuff. Do you have to reference followers in the real method you are calling from after_save ?
Updated code to stub out callback. Now model can remain as you need it, we show #thing.followers is indeed set as we expected, and we can investigate the functionality of do_stuff / some_function via after_save in a different spec.
I pushed a copy of the code here: https://github.com/kikuchiyo/RspecHasMany
And spec passing thing* code is below:
# thing_spec.rb
require 'spec_helper'
describe Thing do
before :each do
Thing.any_instance.stub(:some_function) { puts 'stubbed out...' }
Thing.any_instance.should_receive(:some_function).once
#thing = Thing.create(:name => "Foo");
#user = User.create(:name => "Fred");
#user.things << #thing
end
it "should have created a relationship" do
#thing.followers.should == [#user]
puts #thing.followers.inspect
end
end
# thing.rb
class Thing < ActiveRecord::Base
after_save :some_function
has_many :user_following_thing_relationships
has_many :followers, :through => :user_following_thing_relationships, :source => :user
def some_function
# well, lets me do this, but I cannot use #x without breaking the spec...
#x = followers
puts 'testing puts hear shows up in standard output'
x ||= 1
puts "testing variable setting and getting here: #{x} == 1\n\t also shows up in standard output"
begin
# If no stubbing, this causes rspec to fail...
puts "accessing followers here: #x = #{#x.inspect}..."
rescue
puts "and this is but this is never seen."
end
end
end
My guess is that you need to reload your Thing instance by doing #thing.reload (I'm sure there's a way to avoid this, but that might get your test passing at first and then you can figure out where you've gone wrong).
Few questions:
I don't see you calling #thing.save in your spec. Are you doing that, just like in your console example?
Why are you calling t.save and not u.save in your console test, considering you're pushing t onto u? Saving u should trigger a save to t, getting the end result you want, and I think it would "make more sense" considering you are really working on u, not t.

factory girl and nested_attributes in rails 3

I have 2 models, one which accepts attributes for the other and I'm trying to find a clever way to use Factory girl to setup the data for both.
Class Booking
has_many :booking_items
accepts_nested_attributes_for :booking_items
Class BookingItem
belong_to :booking
my factory
Factory.define :booking do |f|
f.bookdate Date.today+15.days
f.association :space
f.nights 2
f.currency "EUR"
f.booking_item_attributes Factory.build(:booking_item) # doesn't work
end
Factory.define :booking_item do |f|
f.association :room
f.bookdate Date.today
f.people 2
f.total_price 20
f.association :booking
end
booking_spec
require "spec_helper"
describe Booking do
before(:each) do
#booking = Factory.create(:booking)
end
it "should be valid" do
#needs children to be valid
#booking.should be_valid
end
end
I looked around the rdocs but couldn't seem to find what I was looking for.
If I understood you correctly, you want to do this, but with terser syntax:
booking_item = Factory(:booking_item, :people => 4)
booking = Factory(:booking, :booking_item => booking_item)
Of cause you can shortcut it like this:
def with_assocs factory, assocs_hashes = {}, attrs = {}
assoc_models = Hash[ assocs_hash.map { |k, v| [k, Factory(k, v)] } ]
Factory factory, attrs.merge(assoc_models)
end
And use like this:
#booking = with_assocs :booking, :booking_item => {:people => 3}
#booking.should be_valid
In active_factory plugin with similar factory definitions it would look like this:
models { booking - booking_item(:people => 3) }
booking.should be_valid
Unfortunately I haven't yet implemented integration with factory_girl. Though if you interested any input is very welcome.

How to test a before_save method including accossioations with rspec

I'm having a problem testing the following model:
class Bill < ActiveRecord::Base
belongs_to :consignee
before_save :calc_rate
def calc_rate
self.chargeableweight = self.consignee.destination.rate * self.weight
end
end
The consignee model:
class Consignee < ActiveRecord::Base
belongs_to :destination
has_many :bills
end
The controllers are not touched yet.
The behavior of the app is correct (follow up question: are there any performance problems with that solution?) - but the the test break.
You have a nil object when you didn't
expect it! You might have expected an
instance of Array. The error occurred
while evaluating nil.*
Thank you in advice,
Danny
update:
This bill test breaks using factory girl:
describe Bill do
it "should call the calc_rate method" do
bill = Factory.build(:bill)
bill.save!
bill.should_receive(:calc_rate)
end
end
You have a nil object when you didn't expect it!
Factories:
Factory.define :destination do |f|
f.airport_code "JFK"
end
Factory.define :consignee do |f|
...
f.association :destination
end
Factory.define :bill do |f|
f.association :consignee
f.weight 10
f.chargeableweight 20.0
f.after_create do |bill|
bill.calc_rate
end
describe Consignee do
it "should calculate the rate" do
#pending
#make sure this spec is passing first, so you know your calc_rate method is fine.
end
it "should accept calc_rate before save" do
cosignee = mock("Consignee")
consignee.should_receive(:calc_rate).and_return(2) # => stubbing your value
end
end
I didn't spool up a rails app to test this code, but this should get you close. also, assuming that the columns chargeable_rate, weight, etc are columns on the model, you dont need to call self. Ruby will implicitly expect self if there is no instance method or variable of that name available it will automatically look for class methods.

Nested Resource testing RSpec

I have two models:
class Solution < ActiveRecord::Base
belongs_to :owner, :class_name => "User", :foreign_key => :user_id
end
class User < ActiveRecord::Base
has_many :solutions
end
with the following routing:
map.resources :users, :has_many => :solutions
and here is the SolutionsController:
class SolutionsController < ApplicationController
before_filter :load_user
def index
#solutions = #user.solutions
end
private
def load_user
#user = User.find(params[:user_id]) unless params[:user_id].nil?
end
end
Can anybody help me with writing a test for the index action? So far I have tried the following but it doesn't work:
describe SolutionsController do
before(:each) do
#user = Factory.create(:user)
#solutions = 7.times{Factory.build(:solution, :owner => #user)}
#user.stub!(:solutions).and_return(#solutions)
end
it "should find all of the solutions owned by a user" do
#user.should_receive(:solutions)
get :index, :user_id => #user.id
end
end
And I get the following error:
Spec::Mocks::MockExpectationError in 'SolutionsController GET index, when the user owns the software he is viewing should find all of the solutions owned by a user'
#<User:0x000000041c53e0> expected :solutions with (any args) once, but received it 0 times
Thanks in advance for all the help.
Joe
EDIT:
Thanks for the answer, I accepted it since it got my so much farther, except I am getting another error, and I can't quite figure out what its trying to tell me:
Once I create the solutions instead of build them, and I add the stub of the User.find, I see the following error:
NoMethodError in 'SolutionsController GET index, when the user owns the software he is viewing should find all of the solutions owned by a user'
undefined method `find' for #<Class:0x000000027e3668>
It's because you build solution, not create. So there are not in your database.
Made
before(:each) do
#user = Factory.create(:user)
#solutions = 7.times{Factory.create(:solution, :owner => #user)}
#user.stub!(:solutions).and_return(#solutions)
end
And you mock an instance of user but there are another instance of User can be instanciate. You need add mock User.find too
before(:each) do
#user = Factory.create(:user)
#solutions = 7.times{Factory.create(:solution, :owner => #user)}
User.stub!(:find).with(#user.id).and_return(#user)
#user.stub!(:solutions).and_return(#solutions)
end
I figured out my edit, when a find is done from the params, they are strings as opposed to actual objects or integers, so instead of:
User.stub!(:find).with(#user.id).and_return(#user)
I needed
User.stub!(:find).with(#user.id.to_s).and_return(#user)
but thank you so much shingara you got me in the right direction!
Joe

Resources