Rspec stub_const race condition - ruby-on-rails

I'm writing rspec tests like so
describe Module do
describe "method" do
context "first_context" do
before do
stub_const("Module::CONST", "stub0")
end
# logic
end
context "second_context" do
before do
stub_const("Module::CONST", "stub0 stub1")
end
# logic
end
end
end
and about 75% of the time the tests pass as the stub_const logic is working, but 25% of the time a race condition fails and the stub_const from the first test flows in to the const for the second test, so the second test's Module::CONST value is "stub0". Why is this happening?

I've seen this sort of thing happen on JRuby. You can try adding explicit locking around any code that stubs globals, or running each of the examples under a lock:
$lock = Mutex.new
around do |example|
$lock.synchronize do
example.run
end
end
Ensure this is before your before hooks.

Related

Passing argument to a rspec test example

I have some tests that are using a big, test database. I'm also using Database Cleaner to clean the database after each tests. And here comes the problem.
In my spec helper I have this
config.around(:each) do |example|
MongoLib.new(database: "#{Rails.env}_sensor_data").drop_tables!
DatabaseCleaner.cleaning do
example.run
end
end
But, here's the problem. The mentioned group of tests (a big group), generates and drops this big database over and over again (once for each test). That takes a long time, and those tests does not change the database at all, so I don't really want to clean and create the database every time.
So, is there a way to do something like this:
it 'something', argument do
#testing
end
So in the spec helper I can do something like this:
config.around(:each) do |example|
MongoLib.new(database: "#{Rails.env}_sensor_data").drop_tables!
if example.argument?
DatabaseCleaner.cleaning do
example.run
end
end
end
Or maybe there is other solution for that problem? Any ideas?
You've got the right idea. Each example object in your around hook has the metadata method that returns a hash. So you can tag the tests you want to run the cleaner on, and look for that tag in your hook. Something like this:
it "does something", :db_clean do
# ...
end
config.around(:each) do |example|
if example.metadata[:db_clean]
# ...
else
# ...
end
end
You can learn more about these filters here.

Seed data for test

I need to seed some geographical data for my test, and I'm not sure that I'm taking the right approach here, but here is how I've tried.
In my spec helper:
config.before(:each, location_data: true) do |example|
address = FactoryGirl.create(:address, atitude: 20.9223, longitude: -72.551)
end
A specific address point I created. Then I have these, which I think are ruining my data :
config.before(:suite) do
DatabaseCleaner.strategy = :truncation
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
Then in my test I have this:
context 'nearby', location_data: true do
context 'there should be 0 matches in radius' do
binding.pry
#When I debug here there are 0 addresses created
expect(Address.count).to eq(1)
end
end
When I look at the test log its like my test data setup is not even executed, what am I doing wrong here? I need this address for various tests, not just one but many complex scenarios and I would reuse the addresses in another tests, that's why I put them in a rspec config to make it more DRY
Changes as suggested in max answer :
module LocationData
extend ActiveSupport::Concern
included do
let!(:address) { FactoryGirl.create(:address, latitude: 20.9223, longitude: -72.551) }
end
end
Then in my test:
require 'support/location_data'
describe MyModel do
include LocationData
context 'nearby' do
context 'there should be 1 matches in radius' do
binding.pry
#When I debug here there are 0 addresses created
expect(Address.count).to eq(1)
end
end
end
Still get 0 address count when I count addresses. Not sure what am I doing wrong.
SOLUTION (thanks max):
I was missing it block in context block :
context 'there should be 1 matches in radius' do
binding.pry
#When I debug here there are 0 addresses created
it 'has one address before' do
expect(Address.count).to eq(1)
end
end
A good test suite will empty the database between each example.
Why? Stuffing a bunch of data into your database and then running some test on the same DB data sounds like a good idea at first. But if you tests alter that data than you soon end up with ordering issues which can cause flapping tests and serious headaches. Its an approach that has been tested and found lacking.
Instead you want a clean slate for each test. DatabaseCleaner does just that. It's not ruining your data - it's keeping your data from ruining your test suite and or sanity.
You never want to create test data in your rspec configuration. Use it to setup the tools you need to run your test. If you start creating a bunch of flags to set up data from your config it's going to get out of control quickly. And you don't really need the exact same data as often as you think.
Instead if you find yourself repeatedly setting up the same data in your specs you can dry it out with example groups. Or create named factories with FactoryGirl.
module GeocodedExampleGroup
extend ActiveSupport::Concern
included do
let(:address) { FactoryGirl.create(:address, latitude: 20.9223, longitude: -72.551) }
end
end
require 'rails_helper'
require 'support/example_groups/geocoded'
describe SomeModel do
include GeocodedExampleGroup
# ...
end

How can you monkey patch a controller in rspec?

Scenario
Have a race case where concurrency can cause a duplicate key error. Take for example:
def before_create_customer_by_external_id
end
def create_customer_from_external_id(external_id = nil)
#customer = current_account.customers.create!(external_id: external_id || #external_id)
end
def set_new_or_old_customer_by_external_id
if #customer.blank?
before_create_customer_by_external_id
create_customer_from_external_id
end
rescue ActiveRecord::RecordInvalid => e
raise e unless Customer.external_id_exception?(e)
#customer = current_account.customers.find_by_external_id(#external_id)
end
The Test
Now, to test the race case (based on the answer to Simulating race conditions in RSpec unit tests) we just need to monkey patch before_create_customer_by_external_id to call create_customer_from_external_id.
The Question
How can you do this without overriding the whole class and getting a "method not found" error?
After some digging, I came up with the following solution:
context 'with race condition' do
it 'should hit race case and do what is expected' do
ControllerToOverride.class_eval do
def before_create_new_customer_by_external_id
create_customer_from_external_id
end
end
# ...expect...
ControllerToOverride.class_eval do
undef before_create_new_customer_by_external_id
end
end
end
I verified that it was hitting the race case by using a code coverage tool and debug statements.
Happy to know if there's a cleaner way here.
Edit 2020-04-24
Per the comment, we should undef this method so it doesn't affect subsequent tests. Ref: https://medium.com/#scottradcliff/undefining-methods-in-ruby-eb7fba21f63f
I did not verify this, as I no longer have this test suite. Please let me know if it does/does not work.
A step on from monkey patching the class is to create an anonymous subclass:
context "with race condition" do
controller(ControllerToOverride) do
def before_create_customer_by_external_id
end
end
it "should deal with it " do
routes.draw { # define routes here }
...
end
end
This is not so very different to your solution but keeps the monkeypatch isolated to that context block.
You may not need the custom routes block - rspec sets up some dummy routes for the rest methods (edit, show, index etc)
If this context is inside a describe ControllerToOverride block then the argument to controller is optional, unless you have turned off config.infer_base_class_for_anonymous_controllers

Setting expectation on resque .perform method. Task is enqueued in a Callback

So based on my understanding, I beleive when you do
Resque.inline = Rails.env.test?
Your resque tasks will run synchronously. I am writing a test on resque task that gets enqueue during an after_commit callback.
after_commit :enqueue_several_jobs
#class PingsEvent < ActiveRecord::Base
...
def enqueue_several_jobs
Resque.enqueue(PingFacebook, self.id)
Resque.enqueue(PingTwitter, self.id)
Resque.enqueue(PingPinterest, self.id)
end
In the .perform methd of my Resque task class, I am doing a Rails.logger.info and in my test, I am doing something like
..
Rails.logger.should_receive(:info).with("PingFacebook sent with id #{dummy_event.id}")
PingsEvent.create(params)
And I have the same test for PingTwitter and PingPinterest.
I am getting failure on the 2nd and third expectation because it seems like the tests actually finish before all the resque jobs get run. Only the first test actually passes. RSpec then throws a MockExpectationError telling me that Rails.logger did not receive .info for the other two tests. Anyone has had experience with this before?
EDIT
Someone mentioned that should_receive acts like a mock and that I should do .exactly(n).times instead. Sorry for not making this clear earlier, but I have my expectations in different it blocks and I don't think a should_receive in one it block will mock it for the next it block? Let me know if i'm wrong about this.
class A
def bar(arg)
end
def foo
bar("baz")
bar("quux")
end
end
describe "A" do
let(:a) { A.new }
it "Example 1" do
a.should_receive(:bar).with("baz")
a.foo # fails 'undefined method bar'
end
it "Example 2" do
a.should_receive(:bar).with("quux")
a.foo # fails 'received :bar with unexpected arguments
end
it "Example 3" do
a.should_receive(:bar).with("baz")
a.should_receive(:bar).with("quux")
a.foo # passes
end
it "Example 4" do
a.should_receive(:bar).with(any_args()).once
a.should_receive(:bar).with("quux")
a.foo # passes
end
end
Like a stub, a message expectation replaces the implementation of the method. After the expectation is fulfilled, the object will not respond to the method call again -- this results in 'undefined method' (as in Example 1).
Example 2 shows what happens when the expectation fails because the argument is incorrect.
Example 3 shows how to stub multiple invocations of the same method -- stub out each call with the correct arguments in the order they are received.
Example 4 shows that you can reduce this coupling somewhat with the any_args() helper.
Using should_receive behaves like a mock. Having multiple expectations on the same object with different arguments won't work. If you change the expectation to Rails.logger.should_receive(:info).exactly(3).times your spec will probably past.
All that said, you may want to assert something more pertinent than what is being logged for these specs, and then you could have multiple targeted expectations.
The Rails.logger does not get torn down between specs, so it doesn't matter if the expectations are in different examples. Spitting out the logger's object id for two separate examples illustrates this:
it 'does not tear down rails logger' do
puts Rails.logger.object_id # 70362221063740
end
it 'really does not' do
puts Rails.logger.object_id # 70362221063740
end

RSpec Stub doesn't cover multiple nested Describe blocks

I have a test suite structured as follows:
describe ... do
[list of dates].each do
describe
before(:all) do
base_date = ...
end
describe ... do
[list of times].each do
describe ... do
before(:all) do
base_time = base_date + ...
DateTime.stub!(:now).and_return(base_time)
end
describe ... do
<test using records within date-time range based on base_time>
end
describe ... do
<another test using records within date-time range based on base_time>
end
end
end
end
end
end
end
The first test has DateTime(now) == base_time, but the second test as DateTime(now) == my computer's date-time, indicating that the stub is no longer in effect. Moving the stub! call into each describe loop resolves the problem, but I would like to understand why it doesn't work as written.
The reason lies probably elsewhere, stubs work fine with multiple nested describe blocks. Maybe :all vs :each is the problem: before(:all) is executed once before all describe blocks are executed, while before(:each) is executed each time before a describe block is executed.
Or maybe it has something to do with stubbing DateTime, have you tried
DateTime.any_instance.stub(:now).and_return(base_time)

Resources