Testing a block with RSpec - ruby

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

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.

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

Alternative to rspec double that does not fail test even if allow receive is not specified for a function

Many times one outcome may have two different consequences that need to be tested with a test double. For example if a network connection is successful I'd like to log a message, and also pass the resource to another object that will store it internally. On the other hand it feels unclean to put these two in one test. For example this code fails:
describe SomeClass do
let(:logger) { double('Logger') }
let(:registry) { double('Registry') }
let(:cut) { SomeClass.new }
let(:player) { Player.new }
describe "#connect" do
context "connection is successful" do
it "should log info" do
logger.should_receive(:info).with('Player connected successfully')
cut.connect player
end
it "should register player" do
registry.should_receive(:register).with(player)
cut.connect player
end
end
end
end
I could specify in each test that the function in the other one might get called, but that looks like unnecessary duplication. In that case I'd rather make this one test.
I also don't like that it's never explicit in the test that a method should NOT be called.
Does anyone know about an alternative that has an explicit 'should_not_receive' message instead of automatically rejecting calls that are not explicitly specified?
RSpec supports should_not_receive, which is equivalent to should_receive(...).exactly(0).times as discussed in this message from the original author of RSpec.

What can I put as my watir-webdriver page "element" in a condition where it's not there?

I'm testing a nightmarish website that in most situations sticks all the important stuff in an iframe.
However, there are other common situations where the system will, annoyingly, open a page in a new tab, but not wrapped in the iframe.
I'm trying to figure out a conditional method that will check for the existence of the iframe and use it, otherwise not.
Here's what I've come up with, so far:
# The browser object...
#br = Watir::Browser.new
"frm" is the conditional method I'm trying to get working...
# Just an example element definition...
def click_my_button
#br.frm.button(id: "button").click
end
I define it in Watir's Container module, like so:
module Watir
module Container
def frm
if frame(id: "iframeportlet").exist?
frame(id: "iframeportlet")
else
# This is the part that I can't figure out.
end
end
end
end
That works fine when the iframe is there, but not surprisingly I get a NilClass error when it's not.
So, my question is: what can go into the else clause to make it work? More broadly, is there perhaps a better way to accomplish this? As you can imagine, I really want to avoid having to define every element in the web site twice.
I figured it out, and it's quite simple. The frm method's else clause just needs a "self"...
else
self
end
That's it. I'd love to know if there are any hidden pitfalls with this approach, though.

Resources