RSpec shared_examples conditionally check for block presence - ruby

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?

Related

Rspec how to mock function call through instance method

I am trying to test a method which uses instance variable to call featureEnabled method. I am trying to write a rspec unit test for this. I am using below setup in the allow statement. Not sure how else to do this
exception: => #<NoMethodError: undefined method `feature_enabled?' for nil:NilClass>
Api.controllers :Customers do
#domain = current_account.domain
def main
t1 = #domain.featureEnabled?("showPages")
blah
Test:
RSpec.describe ApiHelpers do
describe "#find_matching_lists" do
let(:domain) { Domain.new }
it "madarchod3" do
allow(domain).to receive(:featureEnabled?).with("showPages").and_return(true)
end
end
Variables defined in a block are local to this block, they do not exist outside of it.
The problem you're having is that the domain variable you create with let is only visible inside the block passed to describe. Your test must be defined in that describe block for it to access this variable, ie this should work:
describe "#main" do
let(:domain) { Domain.new }
it "check if feature enabled" do
allow(domain).to receive(:featureEnabled?).with("showPages").and_return(true)
end
end

Rspec how to determine if a let block has been defined?

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

Dry::Web::Container yielding different objects with multiple calls to resolve

I'm trying write a test to assert that all defined operations are called on a successful run. I have the operations for a given process defined in a list and resolve them from a container, like so:
class ProcessController
def call(input)
operations.each { |o| container[o].(input) }
end
def operations
['operation1', 'operation2']
end
def container
My::Container # This is a Dry::Web::Container
end
end
Then I test is as follows:
RSpec.describe ProcessController do
let(:container) { My::Container }
it 'executes all operations' do
subject.operations.each do |op|
expect(container[op]).to receive(:call).and_call_original
end
expect(subject.(input)).to be_success
end
end
This fails because calling container[operation_name] from inside ProcessController and from inside the test yield different instances of the operations. I can verify it by comparing the object ids. Other than that, I know the code is working correctly and all operations are being called.
The container is configured to auto register these operations and has been finalized before the test begins to run.
How do I make resolving the same key return the same item?
TL;DR - https://dry-rb.org/gems/dry-system/test-mode/
Hi, to get the behaviour you're asking for, you'd need to use the memoize option when registering items with your container.
Note that Dry::Web::Container inherits Dry::System::Container, which includes Dry::Container::Mixin, so while the following example is using dry-container, it's still applicable:
require 'bundler/inline'
gemfile(true) do
source 'https://rubygems.org'
gem 'dry-container'
end
class MyItem; end
class MyContainer
extend Dry::Container::Mixin
register(:item) { MyItem.new }
register(:memoized_item, memoize: true) { MyItem.new }
end
MyContainer[:item].object_id
# => 47171345299860
MyContainer[:item].object_id
# => 47171345290240
MyContainer[:memoized_item].object_id
# => 47171345277260
MyContainer[:memoized_item].object_id
# => 47171345277260
However, to do this from dry-web, you'd need to either memoize all objects auto-registered under the same path, or add the # auto_register: false magic comment to the top of the files that define the dependencies and boot them manually.
Memoizing could cause concurrency issues depending on which app server you're using and whether or not your objects are mutated during the request lifecycle, hence the design of dry-container to not memoize by default.
Another, arguably better option, is to use stubs:
# Extending above code
require 'dry/container/stub'
MyContainer.enable_stubs!
MyContainer.stub(:item, 'Some string')
MyContainer[:item]
# => "Some string"
Side note:
dry-system provides an injector so that you don't need to call the container manually in your objects, so your process controller would become something like:
class ProcessController
include My::Importer['operation1', 'operation2']
def call(input)
[operation1, operation2].each do |operation|
operation.(input)
end
end
end

Access ExampleGroup scope outside "it" block

I'm trying to DRY my spec using RSpec's Macros, and I encountered a problem.
describe "..." do
let!(:blog) { create(:blog) }
post "/blogs/#{blog.id}/posts" do
# some macros
end
end
I want to get access to blog variable but I don't want to do it inside it { ... } block so would be able to use my macros regardless of the resource (e.g. I want to apply it to blogs, posts, comments, etc).
Is it possible?
I want to get access to blog variable but I don't want to do it inside it { ... } block
Try not to think of let as normally-scoped variable definition. let is a complex helper method for caching the result of a code block across multiple calls within the same example group. Anything you let will only exist within example groups, meaning you can't access a letted "variable" outside it blocks.
require 'spec'
describe "foo" do
let(:bar) { 1 }
bar
end
# => undefined local variable or method `bar'
That said, if you just want to reuse the result of create(:blog) across multiple examples, you can do:
describe "foo" do
let(:blog) { create(:blog) }
it "does something in one context" do
post "/blogs/#{blog.id}/posts"
# specification
end
it "does something else in another context" do
post "/blogs/#{blog.id}/comments"
# specification
end
end

Is it possible to access the subject of the surrounding context in Rspec?

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

Resources