I'm not understanding scoping when using Cucumber in Ruby, especially with regard to instance variables.
For the context of my immediate example, in the Before portion of hooks.rb
the variable @browser
is assigned.
@browser = Watir::Browser.new @browser_selected.to_sym
(where @browser_selected
is usually 'chrome')
In step definitions @browser gets used. As a simple example: @browser.send_keys(:tab)
What I don't understand is what object contains @browser as an attribute. How does it have meaning in this context? I know the code I'm puzzled by is always in a block, and I recognize that each such block is used (via the Given/When/Then message to which it is attached) to be pre-processed in some mysterious way.
Amidst this shrouding in mystery is the scoping of instance variables. How can I know the scope of instances variables within such blocks?
self
in Cucumber steps and hooks is just a Ruby object, the "world", which is used throughout each scenario. The block in each step definition is executed in the context of the world withthe_world.instance_eval
or something similar, meaning that when each block runsself
is the world. So the object to which all of those instance variables belong is the same object, the world. The scope of all of those instance variables is the entire scenario.So it's important to use instance variables sparingly in Cucumber steps, and to make it clear in step names that you're using them (that is, make it clear in step names that they refer to some piece of state). These steps clearly refer to a thing which is saved behind the scenes (i.e. refer to the same instance variable):
That's fine and normal. But it would be terrible if
When I frob the thing
precalculated some expected assertion results and stashed them in instance variables too, andThen the thing should be frobbed
used those instance variables in its assertions.Then the thing should be frobbed
would not work unlessWhen I frob the thing
preceded it, making it less reusable, and that restriction would not be obvious to others writing features and they would become frustrated. (Don't be like my former colleague.)Back to the world: Cucumber makes a new world for each scenario and throws it away at the end so a scenario's instance variables don't affect the next scenario. In plain Cucumber, the world is just an instance of
Object
. In cucumber-rails, it's an instance ofCucumber::Rails::World
(which is interesting to read). Aside from the methods built in to the world in cucumber-rails, the world gets its methods by extending modules (as inthe_world.extend SomeModule
). As with instance variables, all the methods from all the modules that the world extends are jammed on to the same object (the world), so you sometimes need to worry about name conflicts.