Good strategy for testing a lot of reads on the same data with RSpec? - ruby

I am testing a class, with RSpec, that reads data from the database. Depending on arguments, it will not return the same.
My current strategy is something like this:
before do
# create a bunch of data
end
it 'test1' # ...
it 'test2' # ...
it 'test3' # ...
Which of course means that my data will be created before each test.
I would like to be able to create the data once, in the scope of this file, and do my reads on the data.
I tried setting use_transactional_fixtures to false for the file and use database_cleaner but it made my tests twice as slow because I had to re-seed my db before and after the tests.
I was wondering if there was a way to tell rspec "run each of these tests in the same transaction".
Or maybe, since I'm having a hard time finding that, there's a better strategy for that kind of testing?

It looks like using the database_cleaner gem was the right thing to do, I was just doing it wrong. I had set my cleaning strategy to truncation which emptied the db...
On this Github issue, David Chelimsky explains that using database_cleaner is the way to go https://github.com/dchelimsky/rspec-rails/issues/2
My tests now look like this:
before(:all) do
self.use_transactional_fixtures = false
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.start
create_data
end
after(:all) do
DatabaseCleaner.clean
end
def create_data
# create the data...
end
it 'test1' # ...
it 'test2' # ...
it 'test3' # ...
end
The same tests now run in ~3.5s versus ~7s before. I am happy :)
edit: one before :all is enough

Related

Set RAILS_ENV in rspec

Please considering following code:
class MyModel
validate my_validation unless ENV["RAILS_ENV"] == "test"
end
We have a validation that is going to have major effect on HUGE parts of the test-suite. I only want it to be executed in prod, not when running the test suite*... EXCEPT for the actual tests regarding this validation.
So when testing the validation I need to set the ENV["RAILS_ENV"] to something else then test. I tried this in my my_model_spec.rb-file:
it "tests the validation" do
ENV["RAILS_ENV"] = "development"
# Tests the validation..
ENV["RAILS_ENV"] = "test"
end
This sets the variable while in the spec file, BUT where the check is made in my_model.rb the ENV["RAILS_ENV"] still returns "test".
Is there a way to achieve the declaration of ENV["RAILS_ENV"] in the SPEC-file and have that still set when the model code is executed during the example run?
Yes yes, please believe me we have this under control (... I think :D). It is during a maintenance window.
Obligatory:
validate my_validation unless ENV["RAILS_ENV"] == "test"
In 99.9% of cases, this is really not a good idea.
Just felt I needed to make that clear, in case future readers see this post and get funny ideas... (It would be much better to update the test suite to remain valid, e.g. by changing the factories.)
Is there a way to achieve the declaration of ENV["RAILS_ENV"] in the SPEC-file
Yes - you can stub the value:
allow(ENV).to receive(:[]).with('RAILS_ENV').and_return('development')
There are also some other approaches you could consider.
For example, why not call the method directly, for the purpose of running this test?
record = MyModel.new # or using FactoryBot.build / whatever
record.my_validation
Or, you could add a model attribute to forcibly-run the validation:
class MyModel
attr_accessor :run_my_validation
validate my_validation if ENV["RAILS_ENV"] != "test" || run_my_validation
end
# and in the test:
record = MyModel.new # or using FactoryBot.build / whatever
record.run_my_validation = true
expect(record.valid?).to be_true
Yet another approach you could consider, to eliminate rails environment check from the production code, would be to set an environment-specific configuration value. Which, again, you could stub in the spec:
class MyModel
validate my_validation if Rails.configuration.run_my_model_validation
end
# and in the test:
allow(Rails.configuration).to receive(:run_my_model_validation).and_return(true)
Another benefit to the above is that you could enable the validation in development mode, without making any code change to the application.

Rspec - Is it possible to stub a hash

I'm working on some unit tests. One of them use a specific configuration variable as set in my application MyBigApp::Env which looks like:
{:country=>'uk', :another_hosts=>["192.168.99.105"]}
So I access it with MyBigApp::Env.country
However in my unit test I want that country for the test become something.
Using rspec I've seen stub but can't get it to work - any ideas where I'm going wrong:
MyBigApp::Env.stub(:[]).with('country').and_return('gr')
Also tried this (as above shows deprecated):
allow(MyBigApp::Env).to receive('country').and_return('gr')
Infact as a test, I also tried:
my_hash = {:uri=>nil}
allow(my_hash).to receive(:[]).with(:uri).and_return('Over written!')
p my_hash
and that didnt update either - it just returned {:uri=>nil}
As a workaround, at the moment I'm having to save the env var in a temp var in the before(each) block then return it back to the original in the after(each). This feels really risky to me. I'm thinking imagine the service running and someone runs unit tests it could effect the end user in that small instance the test is running.
Any help would be appreciated.
Thanks
Yes it possible, but keep in mind that stub only works when you trigger/call the method that you stubbed/mocked
my_hash = {:uri=>nil}
allow(my_hash).to receive(:[]).with(:uri).and_return('Over written!')
p my_hash[:url] # it will be 'Over written!'
This works for me:
my_hash = {:uri=>nil}
allow(my_hash).to receive(:[]).with(:uri).and_return('Over written!')
expect(my_hash[:uri]).to eq "Over written!"
In your sample test case, you are just calling p my_hash which doesn't actually call the [] method.
In terms of why this isn't working with MyBigApp::Env, well, that really depends on what class it is. Possible whatever method .country is doesn't actually call [].
Really, if you call MyBigApp::Env['country'] and stub MyBigApp::Env to receive [] with 'country', it should work.
In regards to your concern about changing your running application's behavior from the tests ... what kind of tests are these?! Running unit tests against a live production application would be very odd. How do you imagine it would change your production app's code? The Env hash just lives in memory right?
Anyway, you should never have to worry about your tests changing the experience for an 'end user'. Always run tests on a completely quarantined envionment, meaning don't use the same database. Actually, the test database is usually wiped after each test.
Just wanted to suggest a non-stubbing alternative. For example:
def code_under_test
key = 'country'
# ... maybe lots of code
value = MyBigApp::Env[key] # deep inside some classes
# ... lots more code
"This is the #{value}"
end
MyBigApp::Env is hard-coded deep in the code, and the need for stubbing reveals that dependency, and the benefits of OOP encapsulation are lost.
It'd be much easier if this were the case:
def code_under_test(config_vars = MyBigApp::Env)
"This is the #{config_vars['country']}"
end
it 'should return my country value' do
value = previous_code_under_test('country' => 'TEST VALUE')
expect(value).to eq("This is the TEST VALUE")
end
No stubbing required, just plain old method calls.

Problems with rspec scope in before blocks

I've searched for an answer to this but I just can't seem to figure out what's going wrong. I have an api client test that looks like the following:
module MyTests
describe '#update' do
# using a before(:all) block for setup
before(:all) do
#client1 = Client.new
#initial_payload_state = #client1.update.payload
end
context 'with a known starting payload' do
# The payload is some nasty nested json so I grab an existing one
# and then use a helper method to convert it to a full payload.
# Then I update the client with the new payload. I'm using before(:each)
# so I can get the client into this state for every test.
before(:each) do
#full_payload_state = helper_method(#initial_payload_state)
end
context 'alter_payload_1 works' do
# now that I have the payload in its full state I'd like to alter it to
# produce a certain output
before(:all) do
#new_payload_state = alter_payload_1(#full_payload_state)
end
# I now want to update the client with the altered payload and make sure
# it has the same data. The request and response bodies are formatted slightly
# differently in this case.
it 'works' do
#updated_payload_state = #client1.update(#new_payload_state)
expect(payloads_equal?(#full_payload_state, #new_payload_state).to eq true
end
end
context 'alter_payload_2 works' do
before(:all) do
#new_payload_state = alter_payload_2(#full_payload_state)
end
it 'works' do
#updated_payload_state = #client1.update(#new_payload_state)
expect(payloads_equal?(#full_payload_state, #new_payload_state).to eq true
end
end
In reality, my before block for setup is much longer, so I think it makes sense to keep it that way. I tried to use a before(:each) block so I could have the same known state to start each of the alter_payload contexts. The problem is that with this setup, I get a no method error for this line:
#new_payload_state = alter_payload_1(#full_payload_state)
suggesting that #full_payload_state is nil. I'm certain I've got something wrong with respect to scope, but I'm not sure why or how to fix it. Any help greatly appreciated!
Looks like a scope issue with before(:all).
In general, it's wise to stop using before(:all) because it entangles your tests.
Replace your before(:all) lines with before(:each), and this will make each of your tests independent of the others. This will likely help you find your glitch.

Setting a variable once for each time rspec is run

I am writing a Ruby gem that accesses the features of a web based API. I need to set up an object which will be initialized and log into the API just once for each time the tests are run. before(:all) is still excessive because it will run once for every describe block, and what I want is something that universally sets up once for all of the test files.
UPDATE
Just as a follow up, to make the object I was using available in the tests, I had to add a setting to the rspec config like this
config.add_setting :client
config.before(:suite) do
RSpec.configuration.client = TDAmeritradeApi::Client.new
RSpec.configuration.client.login
end
And then in the describe blocks I do this:
let(:client) { RSpec.configuration.client }
I believe you are looking for before(:suite) and you can use it in the config section of your spec_helper.rb.
RSpec.configure do |config|
config.before(:suite) do
# API login
end
end
You can use before(:suite) to run a block of code before any example groups are run. This should be declared in RSpec.configure
Source: http://rubydoc.info/github/rspec/rspec-core/RSpec/Core/Hooks

What is the RSpec equivalent of Behat profiles

I need to alter env variables eg URLS, depending on the environment (alpha, beta, and public/ops) in which the tests are to be run. Are "contexts" within RSpec that allow for this?
I am not sure if I understand the problem you try to solve. Normally RSpec runs in only in the test environment, not in multople environments.
Your test environment is defined in config/environments/test.rb. In than file you define everything that is valid for all kind of tests. For example you will never ever want to send emails from your test suite. Therefore you will find something like this in that file:
config.action_mailer.delivery_method = :test
If there is something that you want to change in your specs (if DelayedJobs should be available or not in the following example), then there are multiple ways to do so. The one I see most is to stub objects to return the values you need for a certain test:
describe 'a complex operation' do
context 'with DelayedJobs' do
# acts like DelayedJobs are configured to run in background
before { Delayed::Worker.stub(:delay_jobs => true) }
it 'creates a DelayedJob' do
...
end
end
context 'without DelayedJobs' do
# acts like DelayedJobs are configured to run immediately
before { Delayed::Worker.stub(:delay_jobs => false) }
it 'calls the complex query' do
...
end
end
end

Resources