Rspec - Is it possible to stub a hash - ruby

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.

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.

My function works in practice but does not pass tests rspec

Perhaps it is my lack of familiarity with rspec but i do not understand what is going on with my test.
I have 2 classes one called Scrape, the other Result (creative) Scrape is a web scraping class that searches a site and scrapes the results from the page, creating a new Result instance from each.
Result instances are stored in a class variable array accessible via Result.all
this works in practice in the actual program, however when I tried to write a test for this behavior it fails.
describe "#scrape_results" do
it "accepts a url scrapes the page and creates a Result for each" do
s = Scrape.new
s.scrape_results(#url)
expect(Result.all.count).not_to eq(0)
end
end
every time i run the test Result.all.count is 0
if i use pry and manually run #scrape_results the test passes.
I appreciate your time, patience, and help
thanks
I notice that you are passing #url to #scrape_results in your test. Unless you are defining that variable inside of the describe block or the test block it will be nil in your test. It is possible that since #url might be something other than nil from wherever you are pry-ing which is causing the Result to be created and the test to pass.

Mocking a Browser for RSpec, Without Test Doubles Leaking

I find mocking things with RSpec to be entirely problematic and I often don't know how much code to include, in terms of it being diagnostic. So I'll start with the situation I have and the code that I've isolated as causing the problem.
I have tests where I need to mock a browser. I have a mock driver I set up like this:
require "watir"
def mock_driver
browser = double("watir")
allow(browser).to receive(:is_a?).with(Watir::Browser).and_return(true)
allow(browser).to receive(:driver).and_return(true)
browser
end
The only problems I have in my test suite are these two tests:
context "an empiric driver is requested" do
it "a watir browser is provided" do
allow(Watir::Browser).to receive(:new).and_return(Empiric.browser)
Empiric.set_browser mock_driver
end
it "the requested watir browser can be shut down" do
#allow(Empiric.browser).to receive(:quit)
Empiric.quit_browser
#allow(mock_browser).to receive(:new).and_return(Empiric.browser)
#Empiric.set_browser mock_driver
end
end
(The commented out bits in the second test are on purpose to illustrate what's going on.)
With that one line in place in the second test, I get the following error on that test:
<Double "watir"> was originally created in one example but has leaked into another
example and can no longer be used. rspec-mocks' doubles are designed to only last for
one example, and you need to create a new one in each example you wish to use it for.
If I entirely comment out the first test above, that error doesn't happen so I know I've isolated the two tests that are interacting with each other.
Okay, now notice the final line of my second test that is commented out. That seems to be what the error is indicating to me. It's saying I need to create a new double in the other. Okay, so I'll change my last test:
it "the requested watir browser can be shut down" do
#allow(Empiric.browser).to receive(:quit)
Empiric.quit_browser
#allow(mock_browser).to receive(:new).and_return(Empiric.browser)
Empiric.set_browser mock_driver
end
So here I've uncommented the last line so I'm establishing the mock_driver in that test and not allowing the code to leak.
That, however, returns exactly the same error on exactly the same test.
I'm not sure if it would help to see the methods that are being called in that test, but here they are. First is set_browser:
def set_browser(app = :chrome, *args)
#browser = Watir::Browser.new(app, *args)
Empiric.browser = #browser
end
And here is quit_browser:
def quit_browser
#browser.quit
end
The fact that RSpec thought one test was "leaking" into the other made me think that perhaps my #browser instance was the problem, essentially being what's persisting between the two tests. But I don't see how to get around that. I thought that maybe if I quit the browser in the first test, that would help. So I changed the first test to this:
it "a watir browser is provided" do
Empiric.quit_browser
allow(Watir::Browser).to receive(:new).and_return(Empiric.browser)
Empiric.start_browser mock_driver
end
That, however, led to the above error being shown on both tests now.
My more likely accurate guess is that I simply don't know how to provide a mock in this context.
I think you have to use allow with the mock and not Watir::Browser.
For example, what happens if you allow the mock browser to receive whatever calls the browser would and have the it return the mock browser?
Right now you're allowing the "Watir::Browser" to receive those messages and that's returning an "Empiric.browser". Looking at your code, I understand why you put that in there but I think that might be what's screwing you up here.
Mocks in RSpec are horrible things that rarely if ever work correctly in situations like this. I would entirely recommend not using the mock_driver that you have set up. Rather, for each of your tests just do something similar to what you are doing in the mock_driver. My guess is you're including the mock driver as part of a shared context and that, too, is another thing that is very fragile in RSpec. Not recommended.
Instead you might want to use contexts to break up your tests. Then for each context block have a before block. I'm not sure if you should use before:all or before:each given that you're simulating a browser. But that way you can set up the browser in the before and tear it down in an after.
But I would recommend getting it working in each test individually first. Even if it's a lot of code duplication. Then once all tests are passing, refactor to put the browser stuff in those before/after blocks.
But, again, don't use mocks. Don't use shared contexts. It never ends well and honestly it makes your tests harder to reason about.
Given some advice from Micah, I wanted to provide an answer with a solution. I ended up doing this:
context "an empiric driver is requested" do
it "a watir browser is provided" do
allow(Watir::Browser).to receive(:new).and_return(Empiric.browser)
allow(Empiric.browser).to receive(:driver).and_return(true)
expect { Empiric.start_browser :some_browser }.not_to raise_error
end
it "the requested watir browser can be shut down" do
allow(Empiric.browser).to receive(:quit)
allow(Watir::Browser).to receive(:new).and_return(Empiric.browser)
allow(Empiric.browser).to receive(:driver).and_return(true)
expect { Empiric.quit_browser }.not_to raise_error
end
end
All of that was needed as it is or I would get some error or other. I removed my mock driver and, per Micah's suggestion, simply tried to incorporate what seemed to work. The above "contraption" is what I ended up with as the sweet spot.
This works in the sense of giving coverage of the methods in question. What was interesting was that I had to add this to my RSpec configuration:
RSpec.configure do |config|
config.mock_with :rspec do |mocks|
mocks.allow_message_expectations_on_nil = true
end
end
I needed to do this because RSpec was reporting that I was calling allowing something that was nil to receive a value.
This brought up some interesting things, if you think about it. I have a test that is clearly passing. And it adds to my code coverage. But is it actually testing the quit action on a browser? Well, not really since it was testing a quit action on something that it thought was nil.
But -- it does work. And it must be calling the lines of code in question because the code coverage, as reported my SimpleCov, indicates that the statements in question have been checked.

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.

how to reset expectations on a mocked class method?

Sorry if this is plain simple. i am new to ruby as well as rspec and it seems rspec is a very 'obscure' world (esp when coming from a .net background).
In my 'spec', i have:
before(:each) do
expect(File).to receive(:exist?).with("dummy.yaml").and_return (true)
end
This works fine for all my 'examples', except one where i want it to return false.
expect(File).to receive(:exist?).with("non_existent.yaml").and_return (false)
This obviously fails my test because although "non_existent.yaml" expectation was met, the "dummy.yaml" was not:
(<File (class)>).exist?("dummy.yaml")
expected: 1 time with arguments: ("dummy.yaml")
received: 0 times
So how can i do a 'Reset' on 'File.exist?' (a class method mock) before i setup the new expectation for it? (... "non_existent.yaml"..)
i googled and it yielded:
RSpec::Mocks.proxy_for(your_object).reset
but this gives me:
NoMethodError:
undefined method `proxy_for' for RSpec::Mocks:Module
I could not find anywhere in the documentation that this is how you should do it, and past behaviors goes to show that this solution might also change in the future, but apparently this is how you can currently do it:
RSpec::Mocks.space.proxy_for(your_object).reset
I would follow #BroiSatse's remark, though, and think about re-designing the tests, aiming to move the expectation from the before block. The before block is meant for setup, as you say, and the setup is a very weird place to put expectations.
I'm not sure how you came to this design, but I can suggest two possible alternatives:
If the test is trivial, and will work anyway, you should create one test with this explicit expectation, while stubbing it for the other tests:
before(:each) do
allow(File).to receive(:exist?).with("dummy.yaml").and_return (true)
end
it "asks if file exists" do
expect(File).to receive(:exist?).with("dummy.yaml").and_return (true)
# do the test...
end
If the expectation should run for every test, since what changes in each scenario is the context, you should consider using shared examples:
shared_examples "looking for dummy.yaml" do
it "asks if file exists" do
expect(File).to receive(:exist?).with("dummy.yaml").and_return (true)
# do the test...
end
end
it_behaves_like "looking for dummy.yaml" do
let(:scenario) { "something which sets the context"}
end
You might also want to ask myron if there is a more recommended/documented solution to reset mocked objects...
This worked for me to unmock a specific method from a class:
mock = RSpec::Mocks.space.proxy_for(MyClass)
mock.instance_variable_get(:#method_doubles)[:my_method].reset
Note: Same logic of
RSpec::Mocks.space.proxy_for(MyClass).reset which resets all methods
Expanding on #Uri Agassi's answer and as I answered on another similar question, I found that I could use RSpec::Mocks.space.registered? to check if a method was a mock, and RSpec::Mocks.space.proxy_for(my_mocked_var).reset to reset it's value.
Here is the example I included in my other answer:
Example: Resetting a mocked value
For example, if we wanted to reset this mock back to it's unmocked
default value, we can use the RSpec::Mocks.space.proxy_for helper to
find our mock, then reset it:
# when
# Rails.configuration.action_controller.allow_forgery_protection == false
# and
# allow(Rails.configuration.action_controller).to receive(:allow_forgery_protection).and_return(true)
RSpec::Mocks.space.registered?(Rails.configuration.action_controller)
# => true
Rails.configuration.action_controller.allow_forgery_protection
# => true
RSpec::Mocks.space.proxy_for(Rails.configuration.action_controller).reset
Rails.configuration.action_controller.allow_forgery_protection
# => false
Notice however that the even though the mock value has been reset, the
mock remains registered?:
RSpec::Mocks.space.registered?(Rails.configuration.action_controller)
# => true
When using "expect_any_instance" I had success using the following method to change the mock (e.g. our example: Putting out a Twitter post and returning a different tweet id)
expect_any_instance_of(Twitter::REST::Client).to receive(:update).and_return(Hashie::Mash.new(id: "12"))
# post tweet
RSpec::Mocks.space.verify_all
RSpec::Mocks.space.reset_all
expect_any_instance_of(Twitter::REST::Client).to receive(:update).and_return(Hashie::Mash.new(id: "12346"))
# post another tweet

Resources