Skip to content
This repository has been archived by the owner on Jul 6, 2021. It is now read-only.

Create a new namespace for each test scenario #40

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ and in your step definition, you might have
Resetting state between scenarios
---------------------------------

Depending on how your test and/or application code is structured, there may be a chance of data persisting between scenarios, which could result in tests that pass or fail unexpectedly. To eliminiate the risk of this, Cucumber TCL will start a new TCL interpreter between every scenario by creating a new instance of the 'framework' object, meaning that the env.tcl file is loaded each time. Whilst this will remove the data leakage risk, it may also cause your scenarios to run slowly if there is a lot of setup required for a scenario to run (eg, setting up fixture data, building a database or loading large amounts of data into memory). To override the default behaviour of starting up a new interpreter, an environment variable can be passed into the 'cucumber' command enabling the sharing of the TCL interpreter via the 'framework' object:
Depending on how your test and/or application code is structured, there may be a chance of data persisting between scenarios, which could result in tests that pass or fail unexpectedly. To eliminate the risk of this, Cucumber TCL will start a new TCL interpreter between every scenario by creating a new instance of the 'framework' object, meaning that the env.tcl file is loaded each time. Whilst this will remove the data leakage risk, it may also cause your scenarios to run slowly if there is a lot of setup required for a scenario to run (eg, setting up fixture data, building a database or loading large amounts of data into memory). To override the default behaviour of starting up a new interpreter, an environment variable can be passed into the 'cucumber' command enabling the sharing of the TCL interpreter via the 'framework' object:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well spotted!


cucumber SHARE_FRAMEWORK=1

Expand Down
4 changes: 2 additions & 2 deletions features/define_a_step.feature
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ Feature: Define a step
"error "Failing Step""
("eval" body line 2)
invoked from within
"eval $existing_step_body" (Tcl::Error)
"eval $step_body" (Tcl::Error)
features/test.feature:3:in `Given failing'

Failing Scenarios:
Expand Down Expand Up @@ -143,7 +143,7 @@ Feature: Define a step
"failing_proc"
("eval" body line 2)
invoked from within
"eval $existing_step_body" (Tcl::Error)
"eval $step_body" (Tcl::Error)
features/test.feature:3:in `Given failing'

Failing Scenarios:
Expand Down
51 changes: 51 additions & 0 deletions features/namespace_per_test.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
Feature: Namespace per test

A new namespace is created for each test run, to prevent leakage of local variables between tests
when the option to share the TCL interpreter between tests is passed to Cucucmber

Rules:
-- The state is maintained between scenarios if an environment variable is passed into Cucumber
-- Locally defined variables are not accessible between test scenarios

Background:
Given a file named "features/test.feature" with:
"""
Feature:
Scenario:
Given I set a global and a local variable
When I print the global variable
And I print the local variable

Scenario:
When I print the global variable
And I print the local variable
"""
And a file named "features/step_definitions/steps.tcl" with:
"""
global g
variable l

Given {^I set a global and a local variable$} {
set ::g value
set l value
}

When {^I print the global variable$} {
puts $l
}

When {^I print the local variable$} {
puts $l
}
"""
And a file named "features/support/env.rb" with:
"""
require 'cucumber/tcl'
"""

Scenario: Local variable not persisted between tests when running 'cucumber' and sharing framework object
When I run `cucumber SHARE_FRAMEWORK=1`
Then it should fail with:
"""
can't read "l": no such variable
"""
4 changes: 4 additions & 0 deletions lib/cucumber/tcl/framework.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ def step_definition_exists?(step_name)
def execute_step_definition(*args)
@tcl.proc('execute_step_definition').call(*args)
end

def next_scenario()
@tcl.proc('next_scenario').call()
end
end
end
end
46 changes: 32 additions & 14 deletions lib/cucumber/tcl/framework.tcl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
namespace eval ::cucumber:: {
variable STEPS [list]
variable TEST
variable SCENARIO 0

# Set a variable to allow this to be more easily tested
if {[info exists env(TEST)] && $::env(TEST) eq 1} {
Expand All @@ -11,6 +12,7 @@ namespace eval ::cucumber:: {
}

namespace export source_files
namespace export next_scenario
namespace export step_definition_exists
namespace export execute_step_definition
namespace export Given
Expand All @@ -21,7 +23,7 @@ namespace eval ::cucumber:: {
}

#
# Define procs to match Gherkin keyworkds that put data in the STEPS array
# Define procs to match Gherkin keywords that put data in the STEPS array
#
proc ::cucumber::Given args {
_add_step {*}$args
Expand Down Expand Up @@ -51,7 +53,7 @@ proc ::cucumber::_add_step args {
error "The parameters for this procedure are regular_expression ?list_of_capture_variables? body"
return 0
}

lappend STEPS [list $re $params $body]
}

Expand All @@ -72,35 +74,51 @@ proc ::cucumber::execute_step_definition { step_name {multiline_args {}} } {

proc ::cucumber::_search_steps {step_name {execute 0} {multiline_args {}}} {
variable STEPS
variable SCENARIO

foreach step $STEPS {
set existing_step_name [lindex $step 0]
set existing_step_params [lindex $step 1]
set existing_step_body [lindex $step 2]

if {[regexp $existing_step_name $step_name matchresult {*}[join $existing_step_params]]} {
if {[regexp $existing_step_name $step_name]} {
if {$execute == 1} {
set res [::cucumber::scenario_${SCENARIO}::execute_step $existing_step_name $existing_step_params $step_name $existing_step_body $multiline_args]
return $res
} else {
return 1
}
}
}
return 0
}

proc ::cucumber::next_scenario {} {
variable SCENARIO
incr SCENARIO

namespace eval ::cucumber::scenario_$SCENARIO {

proc execute_step {step_signature step_params step_call step_body multiline_args} {
regexp $step_signature $step_call matchresult {*}[join $step_params]
# Now we've found a match, handle multiline args. The name of the var
# should be the last value of the $existing_step_params.
if {$multiline_args ne {}} {
set multiline_var_name [lindex $existing_step_params end]
set multiline_var_name [lindex $step_params end]
set $multiline_var_name $multiline_args
}

if {$execute == 1} {
if {[catch {
eval $existing_step_body
} msg]} {
if {$msg eq "pending"} {
return "pending"
}
error $::errorInfo

if {[catch {
eval $step_body
} msg]} {
if {$msg eq "pending"} {
return "pending"
}
error $::errorInfo
}
return 1
}
}
return 0
}

# Sort a list of files such that: */support/env.{ext} < */support/{file} < */{file}
Expand Down
3 changes: 2 additions & 1 deletion lib/cucumber/tcl/step_definitions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module Tcl
class StepDefinitions
def initialize(tcl_framework)
@tcl_framework = tcl_framework
@tcl_framework.next_scenario
end

def attempt_to_activate(test_step)
Expand All @@ -22,7 +23,7 @@ def match?(test_step)

def action_for(test_step)
arguments = ArgumentList.new(test_step)
proc {
proc {
response = ExecuteResponse.new(@tcl_framework.execute_step_definition(*arguments))
response.raise_any_pending_error
}
Expand Down
Loading