Why am I getting error "#let or #subject called without a block"? - ruby

I wonder how to pass code to shared examples.
I have these specs (snipped)
let(:property) { subject.send(type) }
context 'when one or both depending properties are not set' do
shared_examples_for 'not set' do |init_proc|
it "imperial.value returns nil" do
init_proc.call
expect(property.imperial.value).to eq(nil)
end
# 3 similar examples snipped
end
context "when neither is set" do
include_examples 'not set', Proc.new { }
end
context "when #{type1.inspect} is not set" do
include_examples 'not set', # error in this line
Proc.new { subject.send("#{type2}=", [100, :imperial]) }
end
context "when #{type2.inspect} is not set" do
include_examples 'not set',
Proc.new { subject.send("#{type1}=", [100, :imperial]) }
end
end
What is surprising me is the error I get:
RuntimeError:
#let or #subject called without a block
I thought this would work. It's as if Proc.new calls subject right away, instead when I call init_proc.call. Is that what's going on?
If not, then what's causing the error?
PS. Question is not how to get this code to work. I got this code to work, I am only interested in explanation why I got the error before.

Related

How to use check something before an error is raised

I have the following ruby code
class Gateway
...
def post
begin
...
raise ClientError if state == :open
rescue ClientError => e
Log.add("error")
raise
end
end
end
On RSpec, how can I check that when ClientError is raised Log.add is called?
I have tried different things but I always get the error raised.
Thanks
You can probably do something like this (the initialize step might need to look bit different, depending on how you need to set the state to :open):
describe 'Gateway#post' do
let(:gateway) { Gateway.new(state: :open) }
before { allow(Log).to receive(:add) }
it 'raises an excpetion' do
expect { gateway.post }.to raise_error(ClientError)
expect(Log).to have_received(:add).with('error')
end
end
Something like this should work:
describe '#post' do
context 'with state :open' do
let(:gateway) { Gateway.new(state: :open) }
it 'logs the error' do
expect(Log).to receive(:add).with('error')
gateway.post rescue nil
end
it 're-raises the error' do
expect { gateway.post }.to raise_error(ClientError)
end
end
end
In the first example, rescue nil ensures that your spec is not failing because of the raised error (it silently rescues it). The second example checks that the error is being re-raised.

RSpec it_behaves_like reduces debugging visibility

I have the following code:
context 'user doesnt exists with that email' do
let(:params) { original_params.merge(login: "nouser#example.org") }
it_behaves_like '404'
it_behaves_like 'json result'
it_behaves_like 'auditable created'
end
It is dry because I can use these elements in other contexts as well:
context 'user exists with that email' do
it_behaves_like '200'
it_behaves_like 'json result'
end
My shared_example is:
...
RSpec.shared_examples "json result" do
specify 'returns JSON' do
api_call params, developer_header
expect { JSON.parse(response.body) }.not_to raise_error
end
end
...
The benefits are that the spec is more readable and is dry. The spec failure points to the shared_example file rather than the original spec. It is hard to debug.
The following error occurs at login_api_spec:25, but this is rspecs output:
rspec ./spec/support/shared_examples/common_returns.rb:14 # /api/login GET /email user doesnt exists with that email behaves like 401 returns 401
Any good advice how to proceed to write both dry and easy to debug rspec?
Without shared examples the code would be much longer and not as easy to read:
context 'user doesnt exists with that email' do
let(:params) { original_params.merge(login: "nouser#example.org") }
specify "returns 404" do
api_call params, developer_header
expect(response.status).to eq(404)
end
specify 'returns JSON' do
api_call params, developer_header
expect { JSON.parse(response.body) }.not_to raise_error
end
specify 'creates an api call audit' do
expect do
api_call params, developer_header
end.to change{ EncoreBackend::ApiCallAudit.count }.by(1)
end
end
I have thousands of RSpec tests like this so it is very beneficial to write the specs with shared examples because it is fast to write, but the debugging is hard.
Amongst the detailed errors there is description like this:
Shared Example Group: "restricted_for developers" called from ./spec/api/login_api_spec.rb:194
This tells the exact place of the error

Testing with Rspec - The correct way

My weakest point when it comes to coding, is using TDD & BDD methods - I tend to just write code.. but it is something that I am trying to work on.
Could anyone point out the best way to go about the following problem:
Class1:
module TempMod
class MyClass
def initalize(config)
#config = config
end
def process(xml)
if react_upon? xml.something
puts 'yeah'
else
puts 'nah'
end
end
def react_upon?(xml_code)
#code here
end
end
end
So lets say I wanted to test this class, or build it from a TDD point of view so I write my tests:
describe TempMod::MyClass do
let(:config) {double}
let(:myclass) {TempMod::MyClass.new config}
context 'Given that the xml is something we react upon' do
it 'should check that it is valid' do
myclass.process '<some><xml>here</xml></some>'
end
it 'should output yea'
end
end
How do I test that it is calling the react_upon? method. Do I even want to see it is calling it?
Is the proper way to test it, to test all the functions like the react_upon? itself independently of the other functions?
This is properly the main thing that is most confusing me with this sort of testing. Am I testing the whole class, or just individually testing the functions, and not their interactions with the other functions in that class?
Also I realize the the react_upon? might not adhere to the Single responsibility principle and I would probably move that out to its own module/class which I could test using a stub.
If anyone can shed some light on this for me that would be awesome.
edit:
describe TempMod::MyClass do
let (:valid_planning_status_xml) {
'<StatusUpdate> <TitleId>2329</TitleId> <FromStatus>Proposed</FromStatus> <ToStatus>Confirmed</ToStatus> </StatusUpdate>'
}
let(:config) { double }
let(:status_resolver) { double }
subject(:message_processor) { TempMod::MyClass.new config, status_resolver }
context 'Given that the message XML is valid' do
it 'should check the context of the message' do
expect(message_processor.process valid_planning_status_xml).to call :check_me
end
context 'Given that the message is for a planning event update' do
it 'should call something' do
pending
end
end
context 'Given that the message is for a recording job update' do
end
context 'Given that the message is for a video title update' do
end
end
end
Your question confused me a bit is this what you are asking
module TempMod
class MyClass
def initalize(config)
#config = config
end
def process(xml)
react_upon?(xml.something) ? 'yeah' : 'nah'
end
def react_upon?(xml_code)
#code here
end
end
end
Then test like
describe TempMod::MyClass do
let(:config) {double}
let(:myclass) {TempMod::MyClass.new config}
context 'Given that the xml is something we react upon' do
it "should respond to react_upon?" do
expect(myclass).to respond_to(:react_upon?)
end
it "should react_upon? valid xml" do
expect(myclass.react_upon?(YOUR VALID REACTION GOES HERE)).to be_true
end
it "should not react_upon? invalid xml" do
expect(myclass.react_upon?(YOUR INVALID REACTION GOES HERE)).to be_false
end
it "should say 'yeah' if it is valid" do
expect(myclass.process('<some><xml>here</xml></some>')).to eq('yeah')
end
it "should say 'nah' if it is invalid" do
expect(myclass.process('<some><xml>here</some>')).to eq('nah')
end
it 'should check the context of the message' do
expect(myclass).to receive(:react_upon?).with('<some><xml>here</xml></some>')
myclass.process('<some><xml>here</xml></some>')
end
end
end
Right now your tests have no expectations so I added one that expects myclass to respiond_to the react_upon? method and another that expects myclass.process(xml) to respond with a String that equals yeah.

How can I clear class variables between rspec tests in ruby

I have the following class:
I want to ensure the class url is only set once for all instances.
class DataFactory
##url = nil
def initialize()
begin
if ##url.nil?
Rails.logger.debug "Setting url"
##url = MY_CONFIG["my value"]
end
rescue Exception
raise DataFactoryError, "Error!"
end
end
end
I have two tests:
it "should log a message" do
APP_CONFIG = {"my value" => "test"}
Rails.stub(:logger).and_return(logger_mock)
logger_mock.should_receive(:debug).with "Setting url"
t = DataFactory.new
t = nil
end
it "should throw an exception" do
APP_CONFIG = nil
expect {
DataFactory.new
}.to raise_error(DataFactoryError, /Error!/)
end
The problem is the second test never throws an exception as the ##url class variable is still set from the first test when the second test runs.
Even though I have se the instance to nil at the end of the first test garbage collection has not cleared the memory before the second test runs:
Any ideas would be great!
I did hear you could possibly use Class.new but I am not sure how to go about this.
describe DataFactory
before(:each) { DataFactory.class_variable_set :##url, nil }
...
end
Here is an alternative to the accepted answer, which while wouldn't solve your particular example, I'm hoping it might help a few people with a question in the same vein. If the class in question doesn't specify a default value, and remains undefined until set, this seems to work:
describe DataFactory
before(:each) do
DataFactory.remove_class_variable :##url if DataFactory.class_variable_defined? :##url
end
...
end
Works for me with a class with something more like:
def initialize
##url ||= MY_CONFIG["my value"]
...
end

Rspec let scoping

I believe I have a problem with rspec let and scoping. I can use the methods defined with let in examples (the "it" blocks), but not outside (the describe block where I did the let).
5 describe Connection do
8 let(:connection) { described_class.new(connection_settings) }
9
10 it_behaves_like "any connection", connection
24 end
When I try to run this spec, I get the error:
connection_spec.rb:10: undefined local
variable or method `connection' for
Class:0xae8e5b8 (NameError)
How can I pass the connection parameter to the it_behaves_like?
let() is supposed to be scoped to the example blocks and unusable elsewhere. You don't actually use let() as parameters. The reason it does not work with it_behaves_like as a parameter has to do with how let() gets defined. Each example group in Rspec defines a custom class. let() defines an instance method in that class. However, when you call it_behaves_like in that custom class, it is calling at the class level rather than from within an instance.
I've used let() like this:
shared_examples_for 'any connection' do
it 'should have valid connection' do
connection.valid?
end
end
describe Connection do
let(:connection) { Connection.new(settings) }
let(:settings) { { :blah => :foo } }
it_behaves_like 'any connection'
end
I've done something similar to bcobb's answer, though I rarely use shared_examples:
module SpecHelpers
module Connection
extend ActiveSupport::Concern
included do
let(:connection) { raise "You must override 'connection'" }
end
module ClassMethods
def expects_valid_connection
it "should be a valid connection" do
connection.should be_valid
end
end
end
end
end
describe Connection do
include SpecHelpers::Connection
let(:connection) { Connection.new }
expects_valid_connection
end
The definition of those shared examples are more verbose than using shared examples. I guess I find "it_behave_like" being more awkward than extending Rspec directly.
Obviously, you can add arguments to .expects_valid_connections
I wrote this to help a friend's rspec class: http://ruby-lambda.blogspot.com/2011/02/agile-rspec-with-let.html ...
Redacted -- completely whiffed on my first solution. Ho-Sheng Hsiao gave a great explanation as to why.
You can give it_behaves_like a block like so:
describe Connection do
it_behaves_like "any connection" do
let(:connection) { described_class.new(connection_settings) }
end
end
I've discovered that if you do not explicitly pass the parameter declared by let, it will be available in the shared example.
So:
describe Connection do
let(:connection) { described_class.new(connection_settings) }
it_behaves_like "any connection"
end
connection will be available in the shared example specs
I found what works for me:
describe Connection do
it_behaves_like "any connection", new.connection
# new.connection: because we're in the class context
# and let creates method in the instance context,
# instantiate a instance of whatever we're in
end
This works for me:
describe "numbers" do
shared_examples "a number" do |a_number|
let(:another_number) {
10
}
it "can be added to" do
(a_number + another_number).should be > a_number
end
it "can be subtracted from" do
(a_number - another_number).should be < a_number
end
end
describe "77" do
it_should_behave_like "a number", 77
end
describe "2" do
it_should_behave_like "a number", 2
end
end

Resources