Rspec shared example: Access parameter in a helper method

44 views
Skip to first unread message

Evan Brodie

unread,
Jun 22, 2020, 4:04:20 PM6/22/20
to rspec

I also asked on Stackoverflow (https://stackoverflow.com/questions/62522543/rspec-shared-example-access-parameter-in-a-helper-method), but I'm posting here too because this is the preferred location to ask for rspec help according to your website: http://rspec.info/help/

***********************************

Suppose I create an Rspec shared example group with one parameter (the business purpose of the tests are irrelevant, it is an overly simplified version of my current codebase):

shared_examples "some example group" do |parameter|
  it 
"does something" do
    puts 
"parameter=#{parameter}"


    print_the_parameter
  
end


  
def print_the_parameter
    puts 
"parameter=#{parameter}"
  
end
end

I am able to access the parameter as a variable just fine with the it test block. However, I am running into an "undefined local variable or method" when I try to access parameter from a method. Why is that? I have proven in my codebase (and is prevalently shown in Rspec documentation) that the parameter is available in test blocks, lifecycle methods like before, and in let variable declarations. But why not helper methods?

Thank you.

Jack Royal-Gordon

unread,
Jun 22, 2020, 4:44:40 PM6/22/20
to rs...@googlegroups.com
My understanding (and someone else may be able to clarify or correct this) is that you should think of the code inside the if as an anonymous function to which the “|parameter|” is passed. Therefore, the scope of “parameter” is the “it” block, not the “do” block (as you would think by looking at the code).

The workaround for this situation is to pass the parameter into “print_the_parameter”, in which case you’ll have a local copy to work with.

I’ve run into similar issues with putting code outside the “it” and having it behave in unexpected ways.


On Jun 22, 2020, at 1:02 PM, Evan Brodie <brodi...@gmail.com> wrote:

Also asked on Stackoverflow (https://stackoverflow.com/questions/62522543/rspec-shared-example-access-parameter-in-a-helper-method), but I'm posting here too because this is the preferred location to ask for rspec help according to your website: http://rspec.info/help/

shared_examples "some example group" do |parameter|
  it
"does something" do
    puts
"parameter=#{parameter}"


    print_the_parameter
 
end


 
def print_the_parameter
    puts
"parameter=#{parameter}"
 
end
end
Viewed 3 times

0


Suppose I create an Rspec shared example group with one parameter (the business purpose of the tests are irrelevant, it is an overly simplified version of my current codebase):

  it "does something" do
    puts "parameter=#{parameter}"

    print_the_parameter
  end

  def print_the_parameter
    puts "parameter=#{parameter}"
  end

I am able to access the parameter as a variable just fine with the it test block. However, I am running into an "undefined local variable or method" when I try to access parameter from a method. Why is that? I have proven in my codebase (and is prevalently shown in Rspec documentation) that the parameter is available in test blocks, lifecycle methods like before, and in let variable declarations. But why not helper methods?

Thank you.

--
You received this message because you are subscribed to the Google Groups "rspec" group.
To unsubscribe from this group and stop receiving emails from it, send an email to rspec+un...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/rspec/e3758c6e-df22-40ad-8e7f-6bb50559b1f1o%40googlegroups.com.

Phil Pirozhkov

unread,
Jun 22, 2020, 6:29:26 PM6/22/20
to Jack Royal-Gordon
There's a simple way around this:

let(:parameter) { parameter }

I think there was a rather simple explanation behind this, but it slipped my mind.

Jon Rowe

unread,
Jun 24, 2020, 7:16:37 AM6/24/20
to rs...@googlegroups.com
This is standard Ruby scoping (as I’ve pointed out on stack overflow). Phil’s answer is a great workaround but I strongly encourage not shadowing the name (e.g. call it something else) to prevent confusion around precedence.

I elaborate further on the Stack Overflow post, but shall repeat here.

The parameter passed to the block is available within the block's closure. So the evaluated string `"parameter=#{parameter}"` being within the closure works just fine.

What you're trying to do the same as is this:

```
b = "Hi!"

def a
  puts b
end

a()
# NameError (undefined local variable or method `b' for main:Object)
```

As Phil points out a solution is to wrap parameter in a `let`, which is the same (roughly) as doing:

```
b = "Hi!"

def a
  puts b
end

define_method(:b) { b }

a()
# Hi!
# => nil
```

Cheers
Jon Rowe
---------------------------

Evan Brodie

unread,
Jun 24, 2020, 9:56:07 PM6/24/20
to rspec
I admit that I don't fully understand the example you provided. I'm not sure how it translates into RSpec code, nor what the connection is to "standard Ruby scoping".

Anyhow, what I got out of this thread is that "accessing shared example block variables in a defined method is not possible". I'll use strategies such as exposing the variable in a let variable or passing the values as method parameters. Thank you all for the help.

Jon Rowe

unread,
Jun 25, 2020, 10:31:01 AM6/25/20
to rs...@googlegroups.com
Hi Evan

What I mean by standing ruby scoping are the rules surrounding how and when a variable is accessible.

When a block is defined that takes variables, those variable names are considered to be local to the block (the same as a normal local variable) so are only available within that "scope” e.g.

thing do |local_variable|
  
  # local variable is fine here
  
  method_name do
    # local variable is fine here too, but not in the method definition
  end
end

# but local_variable does not exist here

def method_name
  # nor does local_variable exist here
end

Hope that helps!
Cheers
Jon Rowe
---------------------------

Jack Royal-Gordon

unread,
Jun 25, 2020, 10:46:09 AM6/25/20
to rs...@googlegroups.com
Hi Jon,

I don’t think the original code looks like your example. It has the method definition **inside** the “thing do” block. 


--
You received this message because you are subscribed to the Google Groups "rspec" group.
To unsubscribe from this group and stop receiving emails from it, send an email to rspec+un...@googlegroups.com.

Evan Brodie

unread,
Jun 25, 2020, 11:07:52 AM6/25/20
to rspec
Yes, Jack's analysis is correct. I placed my def method_name method definition within the shared example block, not outside of it.

I do a similar things when I write RSpec tests that used describe and context blocks. I place method definition via def method_name within those blocks and have no issues with accessing any of the let variables or even instance variables of the test. When I'm outside of that describe or context block, then naturally I cannot access that method anymore. Makes perfect sense. Moreover, if I use a method outside of that block, then of course I cannot access let variable from within the block. Neither of these situations are what I'm dealing with here.

Perhaps shared example blocks act differently than describe or context blocks, in that their block parameters are not freely available to nested method definition? I just want to make sure that we are all on the same page though.
To unsubscribe from this group and stop receiving emails from it, send an email to rs...@googlegroups.com.

Jack Royal-Gordon

unread,
Jun 25, 2020, 2:13:03 PM6/25/20
to rs...@googlegroups.com
Hi Evan,

Maybe this text from the documentation explains it: 
"Shared examples let you describe behaviour of types or modules. When declared, a shared group's content is stored. It is only realized in the context of another example group, which provides any context the shared group needs to run.”

So if the context for the code is another example group, then maybe the variables it can access are those defined within that other example group. I don’t know, just trying to make sense of the behavior.

Jack

To unsubscribe from this group and stop receiving emails from it, send an email to rspec+un...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/rspec/b06a8dc3-8502-4f90-a48a-054bf7350cf1o%40googlegroups.com.

Jon Rowe

unread,
Jun 25, 2020, 3:43:46 PM6/25/20
to rs...@googlegroups.com
Hi Evan, Jack

I was attempting to simplify the closure rules for you, that the method was defined in the closure does not change the fact that the local variable cannot be accessed in the scope of the method definition e.g: 

class A; end

A.class_eval do
  a = ‘a’
  def b
    a
  end
end

A.new.b

# NameError (undefined local variable or method `a' for #<A:..>)

This also does not work for similar reasons.

class A
  a = ‘a’
  def b
    a
  end
end

A.new.b
# NameError (undefined local variable or method `a' for #<A:…>)

RSpec defines classes for you, each describe / context is a class, each let is a method on that class, this explains why you can access those, and instance variables inside your methods, you are just defining additional methods on the classes RSpec creates for you.

Each test is run as a `class_exec` within an instance of that class. This is the special case that means you can access the class level local variables (from the shared example blocks) in their definition, you are creating blocks in the scope of the class, and then executing them in the context of the instance.

This is not unique to RSpec, it is Rubies rules of variable scoping.

Hope that helps
Jon Rowe
---------------------------

Filipp Pirozhkov

unread,
Jun 25, 2020, 10:01:10 PM6/25/20
to rspec
I hope this example will help you understand what's going on:

> Class.new { |x| puts x; def a; x; end }.new.a
#<Class:0x00007f96d877eea0>
NameError: undefined local variable or method `x' for #<#<Class:0x00007f96d877eea0>:0x00007f96d877ed88>

This, however, will work as expected:

Class.new { |x| puts x; define_method(:a) do; x; end }.new.a

The reason for this difference is:

> Proc objects are closures, meaning they remember and can use the entire context in which they were created.

This does not relate to methods defined with `def`, they do not remember the context.

Filipp Pirozhkov

unread,
Jun 25, 2020, 10:06:14 PM6/25/20
to rspec
Another thing I can suggest giving a shot is to pass `parameter` as a `let` instead of passing it as a ... ugh ... parameter. This way it will be both available in methods and examples of the shared_examples.

it_behaves_like "some example group" do
  let(:parameter) { ... }

end
Reply all
Reply to author
Forward
0 new messages