Is there a way to automatically update an RSpec expectation? - ruby

I'm wondering if this feature exists in RSpec. I can't seem to find any results when looking into it.
What I'd like to do is something that can be done in Jest tests like so:
// This is a Jest expectation
expect(foo).toMatchInlineSnapshot()
// On the first execution of this code, the value of foo will fill in the expectation and result in something like this
expect(foo).toMatchInlineSnapshot('bar')
I'd love to be able to do this with RSpec tests.
# Here's an RSpec expectation
expect(foo).to eq({ bar: 25 })
Say that I make a change to my code that will result in foo[:bar] having a different value, but I don't know what that value will be.
Currently, I need to re-run my tests and see an error saying something along the lines of
Failure/Error: expect(foo).to eq({ bar: 25 })
expected: { bar: 25 }
got: { bar: 100 }
After that, I need to manually update my expectation in order for it to pass.
Is there anyway to tell RSpec to automatically update the expected value?
For Example:
expect(foo).to eq({ bar: 25 }, {update: true})
would change the code after running the test to match the correct value and result in the following code replacing the above expectation:
expect(foo).to eq({ bar: 100 })
Is there any existing way to accomplish this? Some command that I can run with RSpec maybe?
spec UPDATE_EXPECTS=1
I've seen libraries that can match based on snapshots, but haven't been able to find anything that results in the expected answer appearing inline.
Thanks for the help

I don't believe this type of tool exists for RSpec. The closest you could maybe come is using a let or an instance variable at the top of your script.
let(:subtotal) { 25.0 } # can _maybe_ only update this if logic changed
let(:cart) { Cart.new }
before do
cart.add_item(10.0)
cart.add_item(15.0)
end
it do
expect(cart.subtotal).to eq(subtotal)
end
You'd still have to manually curate the values when logic changes that would break the test assertion. There are already a number of matchers in RSpec that will let you do a looser assertion, such as be_within, but nothing to automatically fill in values for you. The include matcher is another powerful matcher that lets you do looser assertions.
I feel like updating values automatically would be equivalent to testing expect(x).to eq(x). I'm not sure there's much value in that kind of test for what I've typically seen RSpec used for.
The reality is, your test should/will often fail when you make a change to your code. It's annoying that you have to go back and fix the test, but it should also reassuring that the test provided some value. I would be more alarmed if I changed code and didn't have a failing test...

Related

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.

Rspec: id exists in list of id's

I am trying to write a test case where an id exists in the list of id's
I tried following but it doesnt work.
expect(response['products'][0]['ids'][0]['product_id'])
.to include(product_1.id, product_2.id, product_3.id)
It fails everytime with error the expected id doesnt exist.
e.g expected '123' to include '343' and '543'
but when i step through the code all id's are there so dont get why its looking in only in two ids.
response['products'][0]['ids'][0]['product_id'] is a String: "123".
String#include? will return true here, for the following inputs: "", "1", "2", "3", "12", "23", and "123" -- but that's clearly not what you're trying to test!
You wanted to check that this product_id is in that list; not that it includes the list.
This is a slightly unusual test to run, since your expectation is somewhat fuzzy.
If this is a rails application (i.e. you're using ActiveSupport), then you can make use of Object#in? to write the test as follows:
expect(response['products'][0]['ids'][0]['product_id'])
.to be_in(product_1.id, product_2.id, product_3.id)
Or if we're just using vanilla ruby then perhaps use rspec's satisfy matcher:
expect(response['products'][0]['ids'][0]['product_id'])
.to satisfy { |product_id| [product_1.id, product_2.id, product_3.id].include?(product_id) }
You may also be tempted to simply reverse the order of the arguments -- which technically works, but is a little confusing since the code seems like you're running assertions on the wrong object:
expect([product_1.id, product_2.id, product_3.id])
.to include(response['products'][0]['ids'][0]['product_id'])
But back to the point of this being an "unusual test".
Presumably, you've written it this way because you're not certain which order the ids will be listed in - i.e. which product will actually be response['products'][0].
The test would be even better if you either:
Made the order known (but I cannot really advise how without seeing more detail), so that you don't need a fuzzy matcher in the first place, or
Only have 1 product returned by the response in this test, or
Change the test to read all three product_ids from the response, and then use the match_array matcher.

RSpec spy match_array on hash value

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.

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

RSpec 3 acceptance testing approach without "its" blocks

I have a doubt about how to test a simple CSV importer without using the its(:...) clause.
In RSpec 2.x, my approach was to set the imported object as the subject of my spec, and then test each attribute in a its(...) block. It was an acceptance-like test, but it served me well, and I didn't want to unit test the library I used to do my CSV parsing, as it was really a trivial implementation, so I was ok with an end-to-end test.
Now, with RSpec 3, I can make this spec pass with transpec, but I read the explanation about why the its block has been removed and I think RSpec 3 is suggesting a different approach, right? So how would you test that?
I don't think a lot of ugly blocks like this
describe '#email' do
subject { super().email }
it { is_expected.to eq("john_doe#email.com") }
end
are any better than
its(:email) { should == "john.doe#email.com" }
as they do exactly the same thing.
I've read that you need to test "behaviour", but how about acceptance tests? What's the suggested way to go here?
Thanks!
From what I understand, Myron suggests using rspec-given for a one-liner rich test suite. Using this package, your tests will look something like this:
Given(:email) { subject.email }
context "sign up" do
When { subject.sign_up(email: "john.doe#email.com") }
Then { email == "john.doe#email.com" }
end
While the its functionality has been removed from rspec-core, it has been put into an includable gem, rspec-its.
https://github.com/rspec/rspec-its
I would just include this gem and keep writing tests the way you have been - I find them the most readable.
ps. Unrelated but I would also always use eq instead of == in specs :)

Resources