I need to test whether an instance method gets called on a particular instance as a result of calling a class method. Something like:
class Dog < ActiveRecord::Base
def self.roll_trained
Dog.all.each { |d| d.rollover if d.trained? }
end
def rollover
# rollover and stuff
end
def trained?
self.trained == true
end
end
I've written a test like:
describe 'Dog.roll_trained' do
it 'rolls trained dogs' do
dog_1 = Dog.create(trained: true)
dog_1.should_receive(:rollover)
Dog.roll_trained
end
end
I thought I had this right, but the test fails. What am I doing wrong here?
The problem here is that dog_1 and the instances which are looped on the Dog.each is not the same instance.
This is because ActiveRecord generates a different instance for the same row (dog_1) in the database when you do Dog.each
I have written a blog post on this which you can read here: ActiveRecord and In-Memory Object State
One solution would be to stub Dog.all - Dog.stub(all: [dog_1])
Another solution is to save the trained attribute to the database and then spec against the database: dog_1.reload.trained.should be_true
Related
I have this model that my senior dev wrote:
class Thing < ActiveRecord::Base
after_commit on: :create do
SomeMobule.some_method(self)
end
end
I'm wondering how to test this callback.
I've known from the wise of the internet that you can do this:
(in model)
class Thing < ActiveRecord::Base
after_commit :do_something
def do_something
# doing stuff
end
end
(in spec)
it 'fires do_something after commit' do
expect(#instance).to receive(:do_something)
#instance.save
end
But I have no idea how to deal with this callback block.
Method name can be presented in symbol, easy, but what is another module's method name like in symbol? Or there's some other way to receive?
This might come from my lack of Ruby knowledge or for that matter general programming knowledge, and I have no idea even how to pursue the answer on the internet.
You can just test that SomeModule.some_method(self) is called.
let(:thing) { Thing.new }
it 'calls SomeModule.do_something after commit' do
expect(SomeModule).to receive(:do_something).with(thing)
thing.save
end
Which is fine if SomeModule.do_something is an application boundary such as a client to an external API.
If its not the test is very low value from a BDD standpoint - it only tests how the pieces are glued together - not the actually behaviour. A better test would be to test that the expected behaviour is triggered when you save the model.
# a really contrived example
it 'becomes magical when it is saved' do
expect do
thing.save
thing.reload
end.to change(thing, :magical).from(false).to(true)
end
Below is passing!
Controller code:
class OrdersController
def create
...
#order.save
end
end
Spec code:
describe OrdersController do
it "should call save method" do
Order.any_instance.should_receive(:save)
post :create
end
end
But if only it were that easy... I have some custom job objects that are executed after the save, so the code actually looks like this:
Controller code:
class OrdersController
def create
...
#order.save
RoadrunnerEmailAlert.new.async.perform(#order.id, true)
CalendarInvite.new.async.perform(#order.id)
RoadrunnerTwilioAlert.new.async.perform(#order.id)
end
end
I would love to test that the custom objects are receiving the chain of methods with the right parameters, but not sure how, short of creating something in the spec code like this:
before do
class RoadrunnerEmailAlert
def async
end
end
end
But that's so contrived, it certainly isn't right... advice appreciated!
In case this helps other people... this is a very comprehensive answer.
Context & design notes
The async framework is Sucker Punch gem
(http://brandonhilkert.com/blog/why-i-wrote-the-sucker-punch-gem/).
Back then, this was the easiest thing for me to use after looking at
Delayed Job, Sidekick, etc
Basically it works like this: in Controller reference a Job that then references anything else (in my case, some POROs)
If I were really rigidly testing, I'd want to test that A) the Controller calls the Job appropriately and passes the right parameters, and B) the Job calls the appropriate POROs and passes the right parameters. But instead, I just tested that the Controller calls the appropriate POROs and passes the right parameters, i.e., the Jobs are already working.
Controller code
#order.save
RoadrunnerEmailAlert.new.async.perform(#order.id, true)
CalendarInvite.new.async.perform(#order.id)
RoadrunnerTwilioAlert.new.async.perform(#order.id)
Job code
# app/jobs/roadrunner_email_alert.rb
class RoadrunnerEmailAlert
include SuckerPunch::Job
def perform(order_id, require_tos)
ActiveRecord::Base.connection_pool.with_connection do
OrderMailer.success_email(order_id, require_tos).deliver
end
end
end
# app/jobs/calendar_invite.rb
class CalendarInvite
include SuckerPunch::Job
def perform(order_id)
ActiveRecord::Base.connection_pool.with_connection do
CreateCalendar.new(order_id).perform
end
end
end
# app/jobs/roadrunner_twilio_alert.rb
class RoadrunnerTwilioAlert
include SuckerPunch::Job
def perform(order_id)
ActiveRecord::Base.connection_pool.with_connection do
CreateAlert.new(order_id).perform
end
end
end
Test code
The really big thing here that I don't know why I keep forgetting (but only in testing) is class vs. instance of class. For the POROs, since I'm instantiating the object, I needed to test 2 different "layers" (first that the object is instantiated appropriately, second that the instantiated object is acted upon appropriately).
require 'sucker_punch/testing/inline'
describe "Controller code" do
before do
OrderMailer.any_instance.stub(:success_email)
mock_calendar = CreateCalendar.new(1)
CreateCalendar.stub(:new).and_return(mock_calendar)
CreateCalendar.any_instance.stub(:perform)
mock_alert = CreateAlert.new(1)
CreateAlert.stub(:new).and_return(mock_alert)
CreateAlert.any_instance.stub(:perform)
end
it "should call appropriate async jobs" do
expect_any_instance_of(OrderMailer).to receive(:success_email).with(1, true)
expect(CreateCalendar).to receive(:new).with(1)
expect_any_instance_of(CreateCalendar).to receive(:perform)
expect(CreateAlert).to receive(:new).with(1)
expect_any_instance_of(CreateAlert).to receive(:perform)
post :create
end
end
We are maintaining several Rails-Apps which all pose a similar problem that we don't have a really good solution to: All these apps contain models that need to make a API-Call to an external service in their lifecycle.
Possible cases:
User is subscribed to a Newsletter-subscriber-list, when successfully created
Prices for an offer are synced with an external shopping-system after updating
Product is updated in the Search-Index after updating
What we exprienced to NOT be a good solution: Adding these calls to the after_*callbacks of the model. Since that breaks tests fast, cause all factories now have to deal with the api-calls.
I'm looking for a good way to organize these API-call. How do you guys do this?
Ideas we came up with, which I considered not real ideal:
Moving those callbacks to the controller. Now they get easily forgotten, when creating an object
Spawning an asynchronous worker to handle the api-call. Then every - even small app - needs to have the overhead of a delayed job-queue, like sidekiq.
If you are concerned about testing you could put the callback methods into a separate class and mock the callback class during testing. Here's an example using RSpec, given the following Foo and FooCallbacks classes:
class Foo < ActiveRecord::Base
after_save FooCallbacks
end
class FooCallbacks
def self.after_save
fail "Call to external API"
end
end
You can write and successfully run a spec like this:
describe Foo do
before do
allow(FooCallbacks).to receive(:after_save)
end
it "should not invoke real APIs" do
Foo.create
end
end
This is how I now did it, after the advise:
In Foo:
class Foo < ActiveRecord::Base
before_save Foo::DataSync
end
Foo:DataSynclooks like this:
class Foo::DataSync
def self.before_save(foo)
...do the API-Calls...
end
end
Now for testing in rspec I added this:
To spec_helper.rb:
config.before(:each) do
Foo::DataSync.stub(:before_save)
end
Note that config.before(:suite) will not work, since Foo:DataSync is not loaded at that time.
Now foo_spec.rb contains just this:
describe Foo do
let(:foo) {create(:foo)}
it "will sync its data before every save" do
expect(Foo::DataSync).to receive(:before_save).with(foo)
foo.save
end
end
The Foo::DataSync can be tested like this:
describe Foo::DataSync do
let!(:foo) {create(:foo)}
before do
Foo::DataSync.unstub(:before_save)
end
after do
Foo::DataSync.stub(:before_save)
end
describe "#before_save" do
...my examples...
end
end
I was wondering how to test a find_each call in rspec. I'm used to simply stubbing what I want my models to return so I don't rely on test data in the db like this:
MyClass.stub(:find).and_return(my_mock)
However, in another class I'm doing this:
MyClass.find_each do |instance_of_my_class|
do_stuff_here_on(instance_of_my_class)
end
I find that if I do this:
MyClass.stub(:find_each).and_return([one_mock, two_mock])
in the spec test, the "do stuff here" part is not being executed. Does anyone know how to stub a find_each for rspec testing?
You can use and_yield to make rspec call the block passed to the mock:
MyClass.stub(:find_each).and_yield(one_mock).and_yield(two_mock)
If you need to stub find_each on a verified double and have it loop through a specific array of values, you can do this:
let(:my_relation_with_mocked_find_each) do
relation = instance_double('YourModel::ActiveRecord_Relation')
receive_yield = receive(:find_each)
fake_objs.each do |obj|
receive_yield = receive_yield.and_yield(obj)
end
allow(relation).to receive_yield
relation
end
The whole point of stubbing a method is so that the method returns an expected value and not execute its contents. If you have a bunch of logic within the find_each method, I would recommend moving it to a separate method and testing that logic separately. You can then test that your method is called during execution.
Here's a pretty high level example:
class Example1
def my_method
# some logic
end
end
class Example2
def my_other_method
Example1.find_each(&:my_method)
end
end
Rspec:
describe Example1 do
it "should return something" do
example = Example1.new
example.my_method.should == something
end
end
describe Example2 do
it "should call my_method on Example1" do
example1 = mock(:example1, :my_method => true)
example2 = Example2.new
example1.should_receive(:my_method)
example2.my_other_method
end
end
This should do it:
MyClass.stub(:find_each) {|block|
block.call
[one_mock, two_mock]
}
If do_stuff_here_on isn't globally reachable, e.g. an instance method on some_object, you'll need some instance_eval to get the right scope for the block:
MyClass.stub(:find_each) {|block|
some_object.instance_eval(&block)
[one_mock, two_mock]
}
Suppose you have an ActiveRecord::Observer in one of your Ruby on Rails applications - how do you test this observer with rSpec?
You are on the right track, but I have run into a number of frustrating unexpected message errors when using rSpec, observers, and mock objects. When I am spec testing my model, I don't want to have to handle observer behavior in my message expectations.
In your example, there isn't a really good way to spec "set_status" on the model without knowledge of what the observer is going to do to it.
Therefore, I like to use the "No Peeping Toms" plugin. Given your code above and using the No Peeping Toms plugin, I would spec the model like this:
describe Person do
it "should set status correctly" do
#p = Person.new(:status => "foo")
#p.set_status("bar")
#p.save
#p.status.should eql("bar")
end
end
You can spec your model code without having to worry that there is an observer out there that is going to come in and clobber your value. You'd spec that separately in the person_observer_spec like this:
describe PersonObserver do
it "should clobber the status field" do
#p = mock_model(Person, :status => "foo")
#obs = PersonObserver.instance
#p.should_receive(:set_status).with("aha!")
#obs.after_save
end
end
If you REALLY REALLY want to test the coupled Model and Observer class, you can do it like this:
describe Person do
it "should register a status change with the person observer turned on" do
Person.with_observers(:person_observer) do
lambda { #p = Person.new; #p.save }.should change(#p, :status).to("aha!)
end
end
end
99% of the time, I'd rather spec test with the observers turned off. It's just easier that way.
Disclaimer: I've never actually done this on a production site, but it looks like a reasonable way would be to use mock objects, should_receive and friends, and invoke methods on the observer directly
Given the following model and observer:
class Person < ActiveRecord::Base
def set_status( new_status )
# do whatever
end
end
class PersonObserver < ActiveRecord::Observer
def after_save(person)
person.set_status("aha!")
end
end
I would write a spec like this (I ran it, and it passes)
describe PersonObserver do
before :each do
#person = stub_model(Person)
#observer = PersonObserver.instance
end
it "should invoke after_save on the observed object" do
#person.should_receive(:set_status).with("aha!")
#observer.after_save(#person)
end
end
no_peeping_toms is now a gem and can be found here: https://github.com/patmaddox/no-peeping-toms
If you want to test that the observer observes the correct model and receives the notification as expected, here is an example using RR.
your_model.rb:
class YourModel < ActiveRecord::Base
...
end
your_model_observer.rb:
class YourModelObserver < ActiveRecord::Observer
def after_create
...
end
def custom_notification
...
end
end
your_model_observer_spec.rb:
before do
#observer = YourModelObserver.instance
#model = YourModel.new
end
it "acts on the after_create notification"
mock(#observer).after_create(#model)
#model.save!
end
it "acts on the custom notification"
mock(#observer).custom_notification(#model)
#model.send(:notify, :custom_notification)
end