RSpec spy match_array on hash value - ruby

I have a simple test in an RSpec controller spec that checks to see if the correct message was passed to a delayed job:
it 'sends a message to NotifyFollowersJob with relevant person and split_time data' do
allow(NotifyFollowersJob).to receive(:perform_later)
post :import, params: request_params
split_time_ids = SplitTime.all.ids
person_id = SplitTime.first.effort.person_id
expect(NotifyFollowersJob).to have_received(:perform_later)
.with(person_id: person_id,
split_time_ids: split_time_ids)
end
The test usually passes, but sometimes it fails because the split_time_ids (an Array) are sometimes reversed. I do not care what order the split_time_ids are passed to NotifyFollowersJob, so the test should pass regardless of the order.
If I were testing the contents of the Array alone, I could write:
expect(split_time_ids).to match_array(SplitTime.all.ids)
But I can't figure out how to get similar functionality where the Array is a value of one of several arguments.
Any RSpec masters out there care to give me some guidance?

One idea is to make a block to check the message and pass it to your allow method.
Like this:
allow(NotifyFollowersJob).to receive(:perform_later) do |arg|
# Handle your args to avoid fails because of its order
expect(...)
end
And you should keep the expect(NotifyFollowersJob).to have_received(:perform_later) (without the with verification) just to make sure it is still being called.

It looks like your controller action runs SplitTime.all.ids query and passes them to the worker as args.
If you don't care about the order, maybe you can stub this query, to get the same result all the time?
In addition, it'll make your test faster.

Related

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.

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.

With rspec how do I test a method received a hash parameter in the correct order?

Unfortunately I'm needing to interact with a Soap API. If that's not bad enough, the API has parameter ordering turned on which means, regardless of the fact it's XML, it must be structured in the correct element order.
I'm using Savon so I'm building an ordered hash. However after some refactoring, the real calls stopped working whilst all my tests continued to pass. A typical test looks like this:
it 'should receive correctly ordered hash' do
example_id = 12345789
our_api = Example::ApiGateway.new()
params = {:message=>{'ApiKey' => api_key, 'ExampleId' => example_id}}
Savon::Client.any_instance.should_receive(:call).with(:get_user_details, params).and_return(mocked_response())
our_api.get_user(example_id: example_id)
end
The hash compare totally does not care about the order of the keys so this test passes regardless of the actual hash order received. I'd like the just grab the parameters that the call method receives and then I could compare the ordered keys of each hash but I con't find how to do this.
How do I make sure that the Savon call receives the message hash in the correct order?
So in the next google I found the answer. should_receive can take a block, so I can rebuild my test as
it 'should receive correctly ordered hash' do
example_id = 12345789
our_api = Example::ApiGateway.new()
params = {:message=>{'ApiKey' => api_key, 'ExampleId' => example_id}}
Savon::Client.any_instance.should_receive(:call){ |arg1, arg2|
arg1.should eq(:get_user_details)
#Ensure order here manually
arg2[:message].keys.should eq(params[:message].keys)
mocked_response()
}
our_api.get_user(example_id: example_id)
end
Now my tests will break as expected when the keys get messed with and I can rack up yet more hours to working around other peoples's fragile code...

Split array into comma separated list of values

I'm working on a bit of metaprogramming using send methods quite a bit. I've been successful so far because the methods I'm sending to only take one argument.
Example:
client is an API client
#command is a method on client taken as an option to a CLI utility
#verb is a method on command taken as another option in the CLI
def command_keys
case #command
when "something"
self.command_options.slice(:some, :keys)
end
end
Then I call the API client like this:
client.send(#command).send(#verb, command_keys)
This works since the methods all take a Hash as their argument. The problem I've run into is when I need to send more than 1 parameter in command_keys. What I'm wondering is the best way to handle the command_keys method returning more than 1 value. Example:
def command_keys
case #command
when "something"
return self.command_options[:some], self.command_options[:keys]
end
end
In this case, command_keys returns an Array as expected, but when I try to pass that in the send(#verb, command_options) call, it passes it as an Array (which is obviously expected). So, to make a long story short, is there some easy way to make this condition be handled easily?
I know send(#verb, argument1, argument2) would get me the result I want, but I would like to be able to not have to give my script any more implementation logic than it needs, that is to say I would like it to remain as abstracted as possible.
Use splat. You might have to rethink the code a bit, but something like:
client.send(#command).send(#verb, *all_the_args)

Is there a way to get a stack trace from rspec when a method is unexpectedly called more times than specified?

I setup a mock object and told it to expect a check for nil and to return false:
status = double('status')
status.should_receive(:nil?).and_return(false)
I only expect the call to nil? to occur once, but I got an error in my rspec test, saying that status received nil? twice.
Is there a way to get rspec to show where/how each call occurred?
adding the '--backtrace' option did not work.
Try something like this:
status.should_receive(:nil?).twice { puts caller; false }
This tells rspec to allow two invocations and call the associated block each time. Thecaller method generates a full backtrace which you should be able to analyze onstdout. We also returnfalse to stay on the code-path we're testing.
If the two backtraces are hard to distinguish and you're only interested in the second (unexpected) invocation, then set up two successive expectations:
status.should_receive(:nil?).and_return(false)
status.should_receive(:nil?) { puts caller; false }
Here the double will return false on the first invocation and call the block on the second.
Reference for setting responses on expectations:
https://github.com/rspec/rspec-mocks#setting-responses

Resources