Upgrading Rspec to Rspec 2.99, want to use let connection variable in before/after hooks - ruby

I have a head scratcher. When upgrading rspec I am getting:
DEPRECATION: let declaration `directory` accessed in an `after(:all)` hook
at:
`let` and `subject` declarations are not intended to be called
Now I understand that I cannot use let defined variables in before/after hooks. However, the methods that are used with my test suite uses a connection to preform some REST API action:
let {:connection} {user_base}
after(:all) do
connection.delete_folder
end
My question is this: Is there anyway of getting around this without making every connection an instance variable? I want to avoid calling connection variable each time I want to preform an action e.g.
before(:all) do
#connection = user_base
end
it "adds folder" do
#connection.add_folder
end
it "edits folder" do
#connection.edit_folder
end

I think RSpec wants you to run the block before each example instead of your once before all examples:
let(:connection) { user_base }
after do # short for `after(:each) do`
connection.delete_folder
end
it "adds folder" do
connection.add_folder
end
it "edits folder" do
connection.edit_folder
end

Related

Rspec run before block once before multiple it blocks

I have a context block in rspec with multiple it blocks inside it. I want to run a before block to set up data for the it blocks but this data takes a long time to set up and is being used to read only. rspec before(:each) creates and deletes this data after every it block which takes a long time. before(:all) creates the data at the start of all the tests and doesn't delete it.
Is there any way I can have this data created just within the context block and deleted after?
Since RSpec 3 these are officially named before/after(:{example,context}) (see docs).
What you want to accomplish can be done with a before(:context) where you set up the data and an after(:context) where you clean it up.
RSpec.describe Thing do
before(:context) do
#thing = Thing.new
end
after(:context) do
#thing.delete
end
it "has 0 widgets" do
expect(#thing.widgets.count).to eq(0)
end
it "can accept new widgets" do
#thing.widgets << Object.new
end
it "shares state across examples" do
expect(#thing.widgets.count).to eq(1)
end
end

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

Testing a block with RSpec

I have some code (which I've stripped back for the purposes of the this example) and I'm looking for some advice on the best way to test it with RSpec.
ActionController::Renderers.add :liquid do |obj, options|
# Set the mime type as HTML.
self.content_type ||= Mime::HTML
# Do some clever stuff....
# Render the liquid layout.
self.response_body = layout.render(obj)
end
This block is added to a set through the add() call and executed at a later time.
I'm looking to test the functionality of the block, to ensure it does everything I want it too, such as setting the correct variables (self.content_type) and calling the correct libraries (layout.render).
I've never tested the content of a block like this, so looking some friendly advice on how best to go about it, can I test the block as-is, or does it need refactoring into an external method which can be tested?
You can stub out the addition of the Renderer, and test the call of the block from your code. In the code below I assume that the snippet you gave is inside a method named register_liquid:
let(:obj) { double(:obj) }
let(:options) { double(:options) }
before do
allow(ActionController::Renderers).to receive(:add).with(:liquid).and_yield(obj, options)
end
it 'sets content type' do
expect(subject).to receive(:content_type=).with(Mime::HTML)
subject.register_liquid
end

How do I parameterise RSpec tests so I can test the same behaviour under slightly different conditions

I'm implementing a service that has several different ways it can be accessed:
Using simple query parameters
With parameters encoded as a Javascript object
For some calls both GET and POST are supported, with POST being used when there is large amounts of data being sent to the service.
What's the best way to structure my RSpec tests to avoid unnecessarily repeating code, allowing me to run the same basic assertions each time?
I'm already using shared_examples to capture some comment tests for things like response code, mimetype, etc. But I'm wondering whether there are other options, particularly when I want to invoke the service using all request methods AND a range of expected inputs and outputs.
The way I would do it in this case is to specify the request as a lambda that performs it. That way I can refer to it in my shared specs and set a different one for each type of request.
I like using rspec describe blocks when its sets an expectation, in this case that a particular request method is used. The whole thing will look something like this:
describe FooController do
shared_examples_for "any request" do
it "assigns foo" do
#request.call
assigns[:foo].should == "bar"
end
it "does not change the number of bars" do
#request.should_not change(Bar, :count)
end
end
context "using GET" do
before do
#request = lambda { get "index" }
end
it_should_behave_like "any request"
end
end
An even cleaner way is to use the 'let' construct, although it may be a step too deep in rSpec magic for a novice:
describe FooController do
shared_examples_for "any request" do
it "assigns foo" do
request.call
assigns[:foo].should == "bar"
end
it "does not change the number of bars" do
request.should_not change(Bar, :count)
end
end
context "using GET" do
let(:request) { lambda { get "index" } }
it_should_behave_like "any request"
end
end

Resources