Rspec testing inside a loop - ruby-on-rails

I am trying to test the code inside a loop, how would I go about this:
class MyClass
def initialize(topics, env, config, limit)
#client = Twitter::Streaming::Client.new(config)
#topics = topics
#env = env
#limit = limit
end
def start
#client.filter(track: #topics.join(",")) do |object|
# how would I test the code inside here, basically logical stuff
next if !object.is_a?(Twitter::Tweet)
txt = get_txt(object.text)
end
end
Is there a way to do this?

If think that you can use a double of your Twitter::Streaming::Client that has a method filter and when this method is invoked it returns the desired output:
let(:client) { double 'Twitter Client', filter: twitters }
You will need to built manually the twitters object (sorry by my lack of context but I never used the Twitter client) and then you can make the assertions for the result of the start method.

As you can see, testing that code is quite tricky. This is because of the dependency on the Twitter client gem.
You can go down couple of paths:
Don't test it - the Twitter client gem should provide you with Twitter::Tweet objects. You only test your logic, i.e. get_txt method
Do what #Marcus Gomes said - create a collection double that has the filter method implemented.
What I would prefer to do is to stub the #client.filter call in the spec.
For example, in your spec:
some_collection_of_tweets = [
double(Twitter::Tweet, text: "I'll be back!"),
double(Twitter::Tweet, text: "I dare ya, I double dare ya!")
]
#my_class = MyClass.new(topics, env, config, limit)
allow(#my_class.client).to receive(:filter).and_return(some_collection_of_tweets)
This means that the some_collection_of_tweets collection will be returned every time the class calls #client.filter, and by having the data built by you, you what expectations to set.
One thing that you will have to change is to set an attr_reader :client on the class. The only side effect of this type of testing is that you are tying your code to the interfaces of the Twitter client.
But like everything else... tradeoffs :)
Hope that helps!

Perhaps you could do something like this if you really wanted to test your infinite loop logic?
RSpec.describe MyClass do
subject { MyClass.new(['foo','bar'], 'test', 'config', 1) }
let(:streaming_client) { Twitter::Streaming::Client.new }
describe '#start' do
let(:valid_tweet) { Twitter::Tweet.new(id: 1) }
before do
allow(Twitter::Streaming::Client).to receive(:new)
.with('config').and_return(streaming_client)
end
after { subject.start }
it '#get_txt receives valid tweets only' do
allow(valid_tweet).to receive(:text)
.and_return('Valid Tweet')
allow(streaming_client).to receive(:filter)
.with(track: 'foo,bar')
.and_yield(valid_tweet)
expect(subject).to receive(:get_txt)
.with('Valid Tweet')
end
it '#get_txt does not receive invalid tweets' do
allow(streaming_client).to receive(:filter)
.with(track: 'foo,bar')
.and_yield('Invalid Tweet')
expect(subject).not_to receive(:get_txt)
end
end
end

Related

Rspec mocks and stubs confuse with expect

I have confuse when use mocks and stubs in rspec on rails. I have test like below
require 'rails_helper'
class Payment
attr_accessor :total_cents
def initialize(payment_gateway, logger)
#payment_gateway = payment_gateway
#logger = logger
end
def save
response = #payment_gateway.charge(total_cents)
#logger.record_payment(response[:payment_id])
end
end
class PaymentGateway
def charge(total_cents)
puts "THIS HITS THE PRODUCTION API AND ALTERS PRODUCTION DATA. THAT'S BAD!"
{ payment_id: rand(1000) }
end
end
class LoggerA
def record_payment(payment_id)
puts "Payment id: #{payment_id}"
end
end
describe Payment do
it 'records the payment' do
payment_gateway = double()
allow(payment_gateway).to receive(:charge).and_return(payment_id: 1234)
logger = double('LoggerA')
expect(logger).to receive(:record_payment).with(1234)
payment = Payment.new(payment_gateway, logger)
payment.total_cents = 1800
payment.save
end
end
Ok when I run rspec it works, no problem, but when I try to move expect to last line like below:
payment = Payment.new(payment_gateway, logger)
payment.total_cents = 1800
payment.save
expect(logger).to receive(:record_payment).with(1234)
and I try to run rpsec, it fail, I dont know why expect is last line will fail, I thought that expect always puts in last line before we run something to get result to test. Anyone can explain for me ?
expect(sth).to receive sets a message expectation which is to be satisfied between the call and end of the test, and that expectation is verified after the test finishes. When you move the expect to the last line, expectation is set just at the end of the test and no code is executed to satisfy it so it fails. Unfortunately it means breaking the prepare-execute-test order.
Which is why you should really rarely use expect.to receive and replace it with allow.to receive with expect.to have_received
# prepare
allow(logger).to receive(:record_payment)
# execute
..
# test
expect(logger).to have_received(:record_payment).with(1234)
allow.to receive sets up a mock proxy which starts tracing received messages which then can be explicitly verified by expect.to have_received. Some objects automatically sets their mock proxies, for example you don't need allow.to receive for doubles with predefined responses or spies. In your case, you could write the test like:
payment_gateway = double
allow(payment_gateway).to receive(:charge).and_return(payment_id: 1234)
logger = double('LoggerA', record_payment: nil)
payment = Payment.new(payment_gateway, logger)
payment.total_cents = 1800
payment.save
expect(logger).to have_received(:record_payment).with(1234)
Other notes
I strongly recommend using verifiable_doubles, which will protect you from false positives:
payment_gateway = instance_double(PaymentGateway)
allow(payment_gateway).to receive(:charge).and_return(payment_id: 1234)
This test will now raise an exception if there is no charge method defined on PaymentGateway class - protecting you from your tests passing even in case you rename that method but forgot to rename it in the test and implementation.

How do I 'expect' a chain of methods using Rspec where the first method takes a parameter?

I have a method call in a ruby model that looks like the following:
Contentful::PartnerCampaign.find_by(vanityUrl: referral_source).load.first
Within the models spec.rb file, I'm trying to mock that call and get a value by passing in a param. But I'm having trouble figuring out the correct way of calling it.
At the top of my spec.rb file I have:
let(:first_double) {
double("Contentful::Model", fields {:promotion_type => "Promotion 1"})
}
Within the describe block I've tried the following:
expect(Contentful::PartnerCampaign).to receive_message_chain(:find_by, :load, :first).
and_return(first_double)
expect(Contentful::PartnerCampaign).to receive_message_chain(:find_by, :load, :first).with(vanityUrl: 'test_promo_path').
and_return(first_double)
expect(Contentful::PartnerCampaign).to receive_message_chain(:find_by => vanityUrl: 'test_promo_path', :load, :first).
and_return(first_double)
As you can probably guess, none of these are working. Does anyone know the correct way to do this sort of thing? Is it even possible?
Generally speaking, I prefer not to use stub chains, as they are often a sign that you are violating the Law of Demeter. But, if I had to, this is how I would mock that sequence:
let(:vanity_url) { 'https://vanity.url' }
let(:partner_campaigns) { double('partner_campaigns') }
let(:loaded_partner_campaigns) { double('loaded_partner_campaigns') }
let(:partner_campaign) do
double("Contentful::Model", fields {:promotion_type => "Promotion 1"}
end
before do
allow(Contentful::PartnerCampaign)
.to receive(:find_by)
.with(vanity_url: vanity_url)
.and_return(partner_campaigns)
allow(partner_campaigns)
.to receive(:load)
.and_return(loaded_partner_campaigns)
allow(loaded_partner_campaigns)
.to receive(:first)
.and_return(partner_campaign)
end
This is what I would do. Notice that I split the "mocking" part and the "expecting" part, because usually I'll have some other it examples down below (of which then I'll need those it examples to also have the same "mocked" logic), and because I prefer them to have separate concerns: that is anything inside the it example should just normally focus on "expecting", and so any mocks or other logic, I normally put them outside the it.
let(:expected_referral_source) { 'test_promo_path' }
let(:contentful_model_double) { instance_double(Contentful::Model, promotion_type: 'Promotion 1') }
before(:each) do
# mock return values chain
# note that you are not "expecting" anything yet here
# you're just basically saying that: if Contentful::PartnerCampaign.find_by(vanityUrl: expected_referral_source).load.first is called, that it should return contentful_model_double
allow(Contentful::PartnerCampaign).to receive(:find_by).with(vanityUrl: expected_referral_source) do
double.tap do |find_by_returned_object|
allow(find_by_returned_object).to receive(:load) do
double.tap do |load_returned_object|
allow(load_returned_object).to receive(:first).and_return(contentful_model_double)
end
end
end
end
end
it 'calls Contentful::PartnerCampaign.find_by(vanityUrl: referral_source).load.first' do
expect(Contentful::PartnerCampaign).to receive(:find_by).once do |argument|
expect(argument).to eq({ vanityUrl: expected_referral_source})
double.tap do |find_by_returned_object|
expect(find_by_returned_object).to receive(:load).once do
double.tap do |load_returned_object|
expect(load_returned_object).to receive(:first).once
end
end
end
end
end
it 'does something...' do
# ...
end
it 'does some other thing...' do
# ...
end
If you do not know about ruby's tap method, feel free to check this out
I think you need to refactor the chain in two lines like this:
model = double("Contentful::Model", fields: { promotion_type: "Promotion 1" })
campaign = double
allow(Contentful::PartnerCampaign).to receive(:find_by).with(vanityUrl: 'test_promo_path').and_return(campaign)
allow(campaign).to receive_message_chain(:load, :first).and_return(model)
Then you can write your spec that will pass that attribute to find_by and check the chain.

How to test the number of database calls in Rails

I am creating a REST API in rails. I'm using RSpec. I'd like to minimize the number of database calls, so I would like to add an automatic test that verifies the number of database calls being executed as part of a certain action.
Is there a simple way to add that to my test?
What I'm looking for is some way to monitor/record the calls that are being made to the database as a result of a single API call.
If this can't be done with RSpec but can be done with some other testing tool, that's also great.
The easiest thing in Rails 3 is probably to hook into the notifications api.
This subscriber
class SqlCounter< ActiveSupport::LogSubscriber
def self.count= value
Thread.current['query_count'] = value
end
def self.count
Thread.current['query_count'] || 0
end
def self.reset_count
result, self.count = self.count, 0
result
end
def sql(event)
self.class.count += 1
puts "logged #{event.payload[:sql]}"
end
end
SqlCounter.attach_to :active_record
will print every executed sql statement to the console and count them. You could then write specs such as
expect do
# do stuff
end.to change(SqlCounter, :count).by(2)
You'll probably want to filter out some statements, such as ones starting/committing transactions or the ones active record emits to determine the structures of tables.
You may be interested in using explain. But that won't be automatic. You will need to analyse each action manually. But maybe that is a good thing, since the important thing is not the number of db calls, but their nature. For example: Are they using indexes?
Check this:
http://weblog.rubyonrails.org/2011/12/6/what-s-new-in-edge-rails-explain/
Use the db-query-matchers gem.
expect { subject.make_one_query }.to make_database_queries(count: 1)
Fredrick's answer worked great for me, but in my case, I also wanted to know the number of calls for each ActiveRecord class individually. I made some modifications and ended up with this in case it's useful for others.
class SqlCounter< ActiveSupport::LogSubscriber
# Returns the number of database "Loads" for a given ActiveRecord class.
def self.count(clazz)
name = clazz.name + ' Load'
Thread.current['log'] ||= {}
Thread.current['log'][name] || 0
end
# Returns a list of ActiveRecord classes that were counted.
def self.counted_classes
log = Thread.current['log']
loads = log.keys.select {|key| key =~ /Load$/ }
loads.map { |key| Object.const_get(key.split.first) }
end
def self.reset_count
Thread.current['log'] = {}
end
def sql(event)
name = event.payload[:name]
Thread.current['log'] ||= {}
Thread.current['log'][name] ||= 0
Thread.current['log'][name] += 1
end
end
SqlCounter.attach_to :active_record
expect do
# do stuff
end.to change(SqlCounter, :count).by(2)

Rails/Rspec: Testing delayed_job mails

Just wondering how to test that actionmailer requests are actually sent to the delayed_job que in rspec.
I would have assumed it was quite simple, but my delayed_job queue doesn't seem to be incrementing. Code below:
Controller:
def create
#contact = Contact.new(params[:contact])
if #contact.save
contactmailer = ContactMailer
contactmailer.delay.contact_message(#contact)
redirect_to(contacts_url)
else
render :action => "new"
end
Spec:
it "queues mail when a contact is created" do
expectedcount = Delayed::Job.count + 1
Contact.stub(:new).with(mock_contact()) { mock_contact(:save => true) }
post :create, :contact => mock_contact
expectedcount.should eq(Delayed::Job.count)
end
Both before and after the call to the controller, the Delayed::Job.count returns 0. I've tried taking the conditional out of the controller, but I still can't get the delayed job count to increment.
Any suggestions appreciated - cheer
You can also test what the jobs will do by running them or turning off queuing.
Tweak config whenever you want (i.e. in a before :each block).
Delayed::Worker.delay_jobs = false
or perform your saved jobs
Delayed::Worker.new.work_off.should == [1, 0]
I have been using this method happily for a while. For one thing, using the new any_instance support in RSpec, you can test your delayed methods effects directly. However, I've found tests that use work_off to be slow.
What I usually do now is:
mock_delay = double('mock_delay').as_null_object
MyClass.any_instance.stub(:delay).and_return(mock_delay)
mock_delay.should_receive(:my_delayed_method)
Then I have a separate spec for my_delayed_method. This is much faster, and probably better unit testing practice -- particularly for controllers. Though if you're doing request specs or other integration-level specs, then you probably still want to use work_off.
I think your mock object is somehow introducing an error -- it's hard to tell exactly how without seeing the definition of the mock_contact method.
In any case, you might try something along these lines:
it "queues mail when a contact is created" do
Contact.stub(:new) { mock_model(Contact,:save => true) }
Delayed::Job.count.should == 0
post :create, {}
Delayed::Job.count.should == 1
end
or the sexier version (caveat: I always end up doing it the non-sexy way):
it "queues mail when a contact is created" do
Contact.stub(:new) { mock_model(Contact,:save => true) }
expect {
post :create, {}
}.to change(Delayed::Job.count).by(1)
end
You can also follow the convention (from Railscast 275) of
ActionMailer::Base.deliveries.last.to.should == user.email
but instead do this:
Delayed::Job.last.handler.should have_content(user.email)
This thread is a bit old, but here is my go at it:
Create a function expect_jobs
def expect_jobs n, time = nil
expect(Delayed::Job.count).to eq(n)
Timecop.travel(time) unless time.nil?
successes, failures = Delayed::Worker.new.work_off
expect(successes).to eq(n)
expect(failures).to eq(0)
expect(Delayed::Job.count).to eq(0)
Timecop.travel(Time.now) unless time.nil?
end
Then simply call it before checking if the callback has done its job. eg:
it "sends a chapter to the admin user" do
post :chapter_to_user, { chapter: #book.chapters.first}
expect_jobs(1)
SubscribeMailer.should have(1).delivery
SubscribeMailer.deliveries.should have(1).attachment
end
This seems to work on my side, and allows me to run both my delayed jobs and my methods.
#zetetic I think we have to pass block in change method here.
It shoulb be like this:
it "queues mail when a contact is created" do
Contact.stub(:new) { mock_model(Contact,:save => true) }
expect {
post :create, {}
}.to change { Delayed::Job.count }.by(1)
end

How do I mock an object in this case? no obvious way to replace object with mock

Suppose I have this very simple method in Store's model:
def geocode_address
loc = Store.geocode(address)
self.lat = loc.lat
self.lng = loc.lng
end
If I want to write some test scripts that aren't affected by the geocoding service, which may be down, have limitations or depend on my internet connection, how do I mock out the geocoding service? If I could pass a geocoding object into the method, it would be easy, but I don't see how I could do it in this case.
Thanks!
Tristan
using rspecs built in mocking and stubbing, you could do something like this:
setup do
#subject = MyClass.new
end
it 'handles geocoder success' do
mock_geo = mock('result', :lat => 1, :lng => 1)
Store.stub!(:geocode).and_return mock_geo
#subject.geocode_address
#subject.lat.should == mock_geo.lat
#subject.lng.should == mock_geo.lng
end
it 'handles geocoder errors' do
Store.stub!(:geocode).and_raise Exception
#subject.geocode_address
#subject.lat.should == _something_reasonable_
#subject.lng.should == _something_reasonable_
end
Using Double-R (RR) https://github.com/btakita/rr, it's simple:
test 'should mock the geocoding service' do
store = Store.new
mock_location = mock(Object.new)
mock_location.lat{1.234}
mock_location.lng{5.678}
mock(Store).geocode.with_any_args{mock_location}
store.geocode_address
assert_equal 1.234, store.lat
assert_equal 5.678, store.lng
end
If there's no way to mock a service then it shows a poor design. The service should be separate from the model (whatever a Store is). You just need to refactor into a more de-coupled system, which will both allow you to mock it, and make the system easier to maintain.

Resources