In Rspec, I want to take advantage of using super() to call a defined let block if it exists or set a new value if it hasn't, I want to use this within a shared_example group but I just can't find how to do this.
I've tried checking if #some_let exists, I've tried checking if a super method of :some_let is owned by the kernel or not and none of them provide anything useful; I can't access instance_methods or instance_method because Rspec won't let me and searching the internet for a method hasn't revealed an answer.
I want to be able to do something like this:
shared_examples 'a shared example' do
let(:some_let) { let_exists?(:some_let) ? super() : some_new_value }
end
is there a method like let_exists? or something to that effect?
Assuming that you call let before including the shared examples, this would work:
shared_examples 'a shared example' do
let(:some) { 'fallback value' } unless method_defined?(:some)
end
describe 'with let' do
let(:some) { 'explicit value' }
include_examples 'a shared example'
it { expect(some).to eq('explicit value') }
end
describe 'without let' do
include_examples 'a shared example'
it { expect(some).to eq('fallback value') }
end
method_defined? checks if a method called some has already been defined in the current context. If not, the method is defined to provide a default value.
Another (usually easier) approach is to always define a default value and to provide the explicit value after including the shared examples: (thus overwriting the default value)
shared_examples 'a shared example' do
let(:some) { 'default value' }
end
describe 'with let' do
include_examples 'a shared example' # <- order is
let(:some) { 'explicit value' } # important
it { expect(some).to eq('explicit value') }
end
describe 'without let' do
include_examples 'a shared example'
it { expect(some).to eq('default value') }
end
Related
Problem:
I am attempting to create a shared example that has a series of base tests and conditionally can run a few more tests if a let was declared in the block.
Background:
I have a response object that is used as the return value for service objects. It has a payload, which by default is an empty hash but it can contain data if the service sets them.
Using a shared example, I can easily cover the base case when a payload is not present. However, some services populate the payload. Given that the payload often references variables defined by let statements, I opted to share the context through a block instead of passing in parameters to the shared context.
Here is what I would like to do, I have mimicked the behavior with doubles so that this snippet is valid and can be used to test. The subject double represents the service object, and execute returns a double representing the response object.
RSpec.describe 'conditional shared_examples' do
shared_examples_for 'a successful Service' do
let(:response) { subject.execute }
it 'returns a success ServiceResponse' do
expect(response.success?).to be(true)
end
it 'contains the payload from the service', if: payload do
expect(response.payload).to eq(payload)
end
end
describe 'base shared_examples without a block' do
subject { double(execute: double(success?: true)) }
it_behaves_like 'a successful Service'
end
describe 'when a block is provided' do
subject { double(execute: double(success?: true, payload: { foo: 'bar' })) }
it_behaves_like 'a successful Service' do
let(:payload) { { foo: 'bar' } }
end
end
end
This fails because payload is not available for the conditional check, here is the error:
NameError:
undefined local variable or method `payload'
This makes sense to me, payload is not available at the ExampleGroup level, it is only available from within an example.
Questions:
Is there a way to check if a block was passed or if a method has been defined through let at the ExampleGroup level?
How can I conditionally check if this it_behaves_like was passed a block or has a specific let method declared?
I have a several models and in this models I have attributes that I don't want to be blank / empty.
I want to heavily test my models on these restrictions using RSpec and Factory Girl.
However I end up with code duplication:
user_spec:
it 'is invalid if blank' do
expect {
FactoryGirl.create(:user, nickname => '')
}.to raise_error(ActiveRecord::RecordInvalid)
end
message_spec:
it 'is invalid if blank' do
expect {
FactoryGirl.create(:message, :re => '')
}.to raise_error(ActiveRecord::RecordInvalid)
end
How can I factor it ?
RSpec provides several ways to do so, such as Shared Examples.
1. Create a file in your [RAILS_APP_ROOT]/support/
Based on your example, you could name this file not_blank_attribute.rb. Then, you just have to move your duplicated code in and adapting it to make it configurable :
RSpec.shared_examples 'a mandatory attribute' do |model, attribute|
it 'should not be empty' do
expect {
FactoryGirl.create(model, attribute => '')
}.to raise_error(ActiveRecord::RecordInvalid)
end
end
2. Use it_behaves_like function in your specs
This function will call the shared example.
RSpec.describe User, '#nickname' do
it_behaves_like 'a mandatory attribute', :User, :nickname
end
Finally, it outputs:
User#nickname
behaves like a mandatory attribute
should not be empty
I want to reuse this shared_examples block across different spec files. I want to extract it into a separate file, and pass in the object so it's not always user. Both things I tried failed, is it possible?
describe User do
before { #user = build_stubbed(:user) }
subject { #user }
shared_examples 'a required value' do |key| # trivial example, I know
it "can't be nil" do
#user.send("#{key}=", nil)
#user.should_not be_valid
end
end
describe 'name'
it_behaves_like 'a required value', :name
end
end
Just require the other file. shared_examples work at the top level, so once defined they are always available; so be careful of naming conflicts.
A lot of RSpec users will put the shared example in spec/support/shared_examples/FILENAME.rb. Then in spec/spec_helper.rb have:
Dir["./spec/support/**/*.rb"].sort.each {|f| require f}
Or on Rails projects
Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
That is listed in the 'CONVENTIONS' section of the shared example docs.
The following code doesn't work, but it best show what I'm trying to achieve
context "this context describes the class" do
subject do
# described class is actually a module here
c = Class.new.extend(described_class)
c.some_method_that_has_been_added_through_extension
c
end
# ... testing the class itself here ...
context "instances of this class" do
subject do
# this doesn't work because it introduces a endless recursion bug
# which makes perfectly sense
subject.new
end
end
end
I also tried to use a local variable in the inner context that I initialized
with the subject, but no luck. Is there any way I can access the subject of a outer scope from within my subject definition in the inner scope?
Using #subject can sometimes cause trouble. It is "primarily intended" for use with the short-hand checks like #its.
It also can make example harder to read, as it can work to mask the name/intent of what you testing. Here's a blog post that David Chelimsky wrote on the topic of #subject and #let and their role in revealing intention: http://blog.davidchelimsky.net/blog/2012/05/13/spec-smell-explicit-use-of-subject/
Try using let, instead
https://www.relishapp.com/rspec/rspec-core/v/2-10/docs/helper-methods/let-and-let
Here is how I would most likely write it.
context "this context describes the class" do
let(:name_of_the_module) { Class.new.extend(described_class) }
before do
c.some_method_that_has_been_added_through_extension
end
# ... testing the class itself here ...
context "instances of this class" do
let(:better_name_that_describes_the_instance) { klass.new }
# ... test the instance
end
end
SIDENOTE
You might want to revisit whether you want to use subject at all. I prefer using #let in almost all cases. YMMV
Something that obviously works is using an instance variable in the inner context and initializing it not with the subject but subject.call instead. Subjects are Procs. Hence, my first approach didn't work.
context "instances of this class" do
klass = subject.call
subject { klass.new }
end
I have been looking for a solution to this, but for different reasons. When I test a method that could return a value or raise an error, I often have to repeat the subject in two contexts, once as a proc for raise_error and once normally.
What I discovered is that you can give subjects names, like lets. This let's you reference an named subject from an outer scope within a new subject. Here's an example:
describe 'do_some_math' do
let!(:calculator) { create(:calculator) }
# proc to be used with raise_error
subject(:do_some_math) {
-> { calculator.do_some_math(with, complicated, args) }
}
context 'when something breaks' do
it { is_expected.to raise_error } # ok
end
context 'when everything works' do
# here we call the named subject from the outer scope:
subject { do_some_math.call } # nice and DRY
it { is_expected.to be_a(Numeric) } # also ok!
end
end
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