Table of Contents
Step
Given
/When
/Then
From
Keyword
In order to automate executing a .feature
file, step implementations need to be
defined which associate a piece of script code with a pattern. Every time a
step is encountered in a .feature
file,
Squish will try to find a pattern matching the step text. If a match is
found, the associated code is executed.
A step implementation is defined by calling a pre-defined Step
function. A simple definition would look like this:
@Step("user starts the addressbook application") def step(context): startApplication("addressbook")
Step("user starts the addressbook application", function(context) { startApplication("addressbook"); });
Step {user starts the addressbook application} {context} { startApplication addressbook }
Step("user starts the addressbook application") do |context| Squish::startApplication("addressbook") end
use Squish::BDD; Step "user starts the addressbook application", sub { startApplication("addressbook"); };
In this example, a step matching the pattern “user starts the addressbook application” will cause Squish to execute
startApplication("addressbook")
startApplication("addressbook");
startApplication addressbook
Squish::startApplication("addressbook")
startApplication("addressbook");
![]() | Note | |||||||||
---|---|---|---|---|---|---|---|---|---|---|
The step text (after stripping any keywords from the text, such as 'Given') has to match the pattern exactly (the match is "anchored" to the start and end of the text). The table below shows which step texts would match the pattern “user starts the addressbook application”:
Note how, leaving aside the initial keywords such as 'Given' or 'When', the step text has to match the pattern exactly: there must not be any text before or after it. |
Defining a step via Step
will always succeed, except in the
following cases:
The pattern given is empty or otherwise malformed — the latter may occur when using regular expressions.
There is already an existing definition for the given pattern.
For script languages in which Step
takes a 'signature' argument:
the signature must at least contain one argument for the context.
Patterns can make use of certain placeholders to extract data from the step text and pass it to the script code. This allows making step-definitions more reusable since it avoids having to hardcode specific values. For instance, here's an improvement to the above step definition which avoids hardcoding the name of the application to start:
@Step("user starts the |word| application") def step(context, appName): startApplication(appName)
Step("user starts the |word| application", function(context, appName) { startApplication(appName); });
Step {user starts the |word| application} {context appName} { startApplication $appName }
Step("user starts the |word| application") do |context, appName| Squish::startApplication(appName) end
use Squish::BDD; Step "user starts the |word| application", sub { my %context = %{shift()}; my $appName = shift(); startApplication($appName); };
The application name addressbook
is no longer hardcoded.
Instead, the |word|
will match the word and pass it via the new
appName
argument.
The following placeholders can be used:
Placeholder | Meaning |
---|---|
|word| |
Matches a single word. A word is one or more characters in length and consists
of letters, digits and underscore (_ ) characters.
|
|integer| | Matches a positive or negative integer value, i.e. a number without a fractional component. |
|any| | Matches a sequence of one or more arbitrary characters. |
Sometimes, you may find that the above mentioned placeholders are insufficient,
that you need more control over which portions of a step text gets extracted.
This can be achieved by not passing a plain string to the Step
function but rather a regular expression. Regular expressions are vastly
more powerful, but also require being more careful - they can easily become
somewhat cryptic to the reader.
Regular expressions not only allow much finer control over the text being matched. They can make use of capturing groups to extract data from the step text and pass it to the step code.
![]() | Python-specific |
---|---|
In Python scripts, you can pass a regular expression object directly (i.e.
the object returned by Python's @Step(r"user starts the (\w+) application", regexp=True) def step(context, appName): ...
We also recommend to use Python's raw string syntax ( |
![]() | Tcl-specific |
---|---|
In Tcl scripts, there are no regular expression values. Instead, the Step -rx "This is a reg.lar expression$" {context} { ... } |
For example, the above examples can be expressed using regular expressions as follows:
@Step(r"user starts the (\w+) application", regexp=True) def step(context, appName): startApplication(appName)
Step(/user starts the (\w+) application/, function(context, appName) { startApplication(appName); });
Step -rx {user starts the (\w+) application} {context appName} { startApplication $appName }
Step(/user starts the (\w+) application/) do |context, appName| Squish::startApplication(appName) end
use Squish::BDD; Step qr/user starts the (\w+) application/, sub { my %context = %{shift()}; my $appName = shift(); startApplication($appName); };
Table of Contents
Each BDD implementation function has a context
singleton
object passed in as the first argument. The context objects of scenario
hooks, step hooks and step implementations provide different properties.
(See Performing Actions During Test Execution Via Hooks (Section 6.19.10) for details on hooks.)
Property | Description |
---|---|
context.title | Title (name) of the current scenario |
context.userData | An extra property that can be used for passing arbitrary data (typically as a list, map or dictionary) between steps and hooks. (Default: None, null, etc.) |
Property | Description |
---|---|
context.text | Text (name) of the current step |
context.userData | An extra property that can be used for passing arbitrary data (typically as a list, map or dictionary) between steps and hooks. (Default: None, null, etc.) |
Property | Description |
---|---|
context.multiLineText | Stores multi-line text if it is being passed into the step. |
context.table | Stores table data if it is being passed into the step. |
context.userData | An extra property that can be used for passing arbitrary data (typically as a list, map or dictionary) between steps and hooks. (Default: None, null, etc.) |
Examples using userData, table and multiLineText are in the following sections.
The context
argument passed to all steps (and hooks) features a
userData field which can be used to pass data between steps. This
property is initially empty but can be written to from within a step. Consider
e.g. a feature file like:
Feature: Shopping Cart Scenario: Adding three items to the shopping cart Given I have an empty shopping cart When I add bananas to the shopping cart And I add apples to the shopping cart And I add milk to the shopping cart Then I have 3 items in the shopping cart
The implicit state in this scenario (the current contents of the shopping cart) can be modeled using userData:
@Given("I have an empty shopping cart") def step(context): context.userData = [] @When("I add |word| to the shopping cart") def step(context, itemName): context.userData.append(itemName) @Then("I have |integer| items in the shopping cart") def step(context, expectedAmount): test.compare(len(context.userData), expectedAmount)
Given("I have an empty shopping cart", function(context) { context.userData = []; }); When("I add |word| to the shopping cart", function(context, itemName) { context.userData.push(itemName); }); Then("I have |integer| items in the shopping cart", function(context, expectedAmount) { test.compare(context.userData.length, expectedAmount); });
Given("I have an empty shopping cart", sub { my $context = shift(); $context->{userData} = []; }); When("I add |word| to the shopping cart", sub { my($context, $itemName) = @_; push @{$context->{userData}}, $itemName; }); Then("I have |integer| items in the shopping cart", sub { my($context, $expectedAmount) = @_; test::compare(scalar(@{$context->{userData}}), $expectedAmount); });
Given "I have an empty shopping cart" {context} { $context userData {} } When "I add |word| to the shopping cart" {context itemName} { set items [$context userData] lappend items $itemName $context userData $items } Then "I have |integer| items in the shopping cart" {context expectedAmount} { set items [$context userData] test compare [llength $items] $expectedAmount }
Given("I have an empty shopping cart") do |context| context.userData = [] end When("I add |word| to the shopping cart") do |context, itemName| context.userData.push itemName end Then("I have |integer| items in the shopping cart") do |context, expectedAmount| Test.compare context.userData.count, expectedAmount end
Here, the userData field keeps track of the items added to the shopping cart, because this is extra data related to the context of our test. It could also be used to keep track of active Application Context (Section 6.3.12) objects in multiple-AUT test cases, for example.
![]() | Note |
---|---|
The userData is never cleared; you can however do so
explicitly using e.g. a |
The context
object exposes optional extra arguments
to the step with these two properties:
Returns an optional multi-line text argument to a step. The text is returned as a list of strings, each of which being a single line. For example, a step such as
Given I create a file containing """ [General] Mode=Advanced """
The multi-line text can be accessed via
@Step("I create a file containing") def step(context): text = context.multiLineText f = open("somefile.txt", "w") f.write("\n".join(text)) f.close()
Step("I create a file containing", function(context) { var text = context.multiLineText; var file = File.open("somefile.txt", "w"); file.write(text.join("\n")); file.close(); });
Step {I create a file containing} {context} { set text [$context multiLineText] set f [open "somefile.txt" "w"] puts $f [join $text "\n"] close $f }
Step("I create a file containing") do |context| File.open("somefile.txt", 'w') { |file| file.write(context.multiLineText.join("\n")) } end
use Squish::BDD; Step "I create a file containing", sub { my %context = %{shift()}; my @multiLineText = @{$context{'multiLineText'}}; my $text = join("\n", @multiLineText); open(my $fh, '>', 'somefile.txt') or die; print $fh $text; close $fh; };
Returns an optional text argument to a step. The table is returned as a list of lists: each inner list represents a row, and individual list elements represent cells. For example, a step such as
Given I enter the records | firstName | lastName | age | | Bob | Smith | 42 | | Alice | Thomson | 27 | | John | Martin | 33 |
The table argument can be accessed via
@Step("I enter the records") def step(context): table = context.table # Drop initial row with column headers for row in table[1:]: first = row[0] last = row[1] age = row[2] # ... handle first/last/age
Step("I enter the records", function(context) { var table = context.table; // Skip initial row with column headers by starting at index 1 for (var i = 1; i < table.length; ++i) { var first = table[i][0]; var last = table[i][1]; var age = table[i][2]; // ... handle first/last/age } });
Step {I enter the records} {context} { set table [$context table] # Drop initial row with column headers foreach row [lreplace $table 0 0] { foreach {first last age} $row break # ... handle $first/$last/$age } }
Step("I enter the records") do |context| table = context.table # Drop initial row with column headers table.shift for first,last,age in table do # ... handle first/last/age end end
use Squish::BDD; Step "I enter the records", sub { my %context = %{shift()}; my @table = @{$context{'table'}}; # Drop initial row with column headers shift(@table); for my $row (@table) { my ($first, $last, $age) = @{$row}; # ... handle $first/$last/$age } };
In addition to Step
, Squish supports defining step
implementations using three more functions, called Given
,
When
and Then
. These three functions follow the same
syntax as Step
but cause a slightly different behavior when
executing steps since step implementations registered using Step
match more step texts than those registered with any of the other three
functions. The following table illustrates the difference:
Step text | Step("...", ..) matches | Given("...", ..) matches | When("...", ..) matches | Then("...", ..) matches |
---|---|---|---|---|
Given I say hello | Yes | Yes | No | No |
When I say hello | Yes | No | Yes | No |
Then I say hello | Yes | No | No | Yes |
And I say hello | Yes | Maybe | Maybe | Maybe |
But I say hello | Yes | Maybe | Maybe | Maybe |
Note how patterns registered via Step
always match, no matter what keyword the
step at hand starts with. Given
, When
and Then
only
match if the step starts with the matching keyword (or a synonym such as "And" or "But").
![]() | Tip |
---|---|
It is generally preferable to use
|
Whether patterns registered via Given
/When
/Then
match steps starting with And
or But
depends on the
keyword preceding the And
/But
.
Consider:
Feature: Some Feature Scenario: Some Scenario Given I am hungry And I'm in the mood for 20000 of something But I do not like peas Then I will enjoy rice.
In this feature description, the lines starting with “And” and
“But” succeed a line starting with “Given”, they are
synonymous. This means that only patterns registered via Step
or
Given
would match the step “And I'm in the mood for 20000 of
something”.
When executing a BDD test, Squish will respect a specific order when deciding which step implementation matches a given step text. Step implementations are considered based on the order in which source files containing step implementations are loaded when starting a BDD test. By default, this order is as follows:
steps
subdirectory of current test
case.
shared/scripts/steps
directory of
current test suite.
This order causes steps defined in the local test case to be preferred over steps defined in the shared scripts directory of the test suite.
Actually, Squish will not only load step implementations stored in
above-mentioned directories but also those stored in subdirectories. For
instance, you can register step implementations in files like
steps/basic/basicsteps.py
or
steps/uisteps/mainwindowsteps.js
.
![]() | Tip |
---|---|
The list of directories from which step implementations are loaded is not
hard-wired into Squish. Instead, it's a list of directories specified in the
|
Even though registering two step implementations with the same pattern will yield an error, registering two step implementations which are defined in different directories will not trigger an error. This means that you can 'override' shared steps by using the exact same pattern in a testcase-specific file.
It is possible to define step implementations such that the current scenario is aborted and all subsequent steps (if any) are skipped. This is useful in case subsequent steps rely on certain conditions (e.g. some file exists) but a preceding step which is supposed to establish/verify that condition fails.
To skip subsequent steps in the current scenario, step implementations can return a special value - the exact name of which depending on the respective scripting language:
@Step("I create a file containing") def step(context): try: text = context.multiLineText f = open("somefile.txt", "w") f.write("\n".join(text)) f.close() except: # Failed to create file; skip subsequent steps in current scenario return AbortScenario
Step("I create a file containing", function(context) { try { var text = context.multiLineText; var file = File.open("somefile.txt", "w"); file.write(text); file.close(); } catch (e) { // Failed to create file; skip subsequent steps in current scenario return AbortScenario; } });
Step {I create a file containing} {context} { if {[catch { set text [$context multiLineText] set f [open "somefile.txt" "w"] puts $f [join $text "\n"] close $f }]} then { # Failed to create file; skip subsequent steps in current scenario return AbortScenario } }
Step("I create a file containing") do |context| begin File.open("somefile.txt", 'w') { |file| file.write(context.multiLineText.join("\n")) } rescue Exception => e # Failed to create file; skip subsequent steps in current scenario next ABORT_SCENARIO end end
use Squish::BDD; Step "I create a file containing", sub { my %context = %{shift()}; my @multiLineText = @{$context{'multiLineText'}}; my $text = join("\n", @multiLineText); if (open(my $fh, '>', 'somefile.txt')) { print $fh $text; close $fh; } else { # Failed to create file; skip subsequent steps in current scenario return ABORT_SCENARIO; } };
It is common that some actions need to be performed before or after some event occurs. This need arises in cases like
Global variables should be initialized before test execution starts.
An application needs to be started before a Feature is executed.
Temporary files should be removed after a Scenario is executed.
Squish allows defining functions which are hooked into the test execution sequence. The functions can be registered with certain events and are executed before or after an event occurs. You can register as many functions for an event as you like: the hook functions will be called in the same order as they were defined. Functions can be associated with any of the following events:
OnFeatureStart
/OnFeatureEnd
These events are raised before/after the first/last scenario in a specific
Feature
is executed. The context
argument passed
to functions associated with either of these events provides a
title
field which yields the title of the feature which is about
to get executed (resp. just finished executing).
OnScenarioStart
/OnScenarioEnd
These events are raised before/after the first/last step in a specific
Scenario
is executed. The OnScenarioStart
event
is also raised before any Background
is executed. The
context
argument passed to functions associated with either of
these events provides a title
field which yields the title of the
scenario which is about to get executed (resp. just finished executing).
OnStepStart
/OnStepEnd
These events are raised before/after the code in a specific
Step
is executed. The context
argument passed
to functions associated with either of these events provides a
text
field which yields the text of the step which is about to get
executed (resp. just finished executing).
You can associate code to be executed in case any of these events occur using
language-specific API. In general, the name of the function
to sign up for an event equals the name of the event except for Python, where
the name of the function doesn't matter since a decorator is used instead. These
functions are registered in the same script files in which you would use the
Step
function to register step implementations.
Here are a few examples:
Setting up a OnFeatureStart
hook to setup global variables:
OnFeatureStart(function(context) { counter = 0; inputFileName = "sample.txt"; });
my $inputFileName; my $counter; OnFeatureStart sub { $counter = 0; $inputFileName = 'sample.txt'; };
@OnFeatureStart def hook(context): global counter global inputFileName counter = 0 inputFileName = "sample.txt"
OnFeatureStart do @counter = 0 @inputFileName = 'sample.txt' end
OnFeatureStart {context} { global counter global inputFileName set $counter 0 set $inputFileName "sample.txt" }
Registering OnScenarioStart
& OnScenarioEnd
event handlers to start & stop an AUT:
OnScenarioStart(function(context) { startApplication("addressbook"); }); OnScenarioEnd(function(context) { currentApplicationContext().detach(); });
OnScenarioStart sub { ::startApplication('addressbook'); }; OnScenarioEnd sub { ::currentApplicationContext.detach(); };
@OnScenarioStart def hook(context): startApplication("addressbook") @OnScenarioEnd def hook(context): currentApplicationContext().detach()
OnScenarioStart do |context| Squish::startApplication 'addressbook' end OnScenarioEnd do |context| Squish::currentApplicationContext.detach end
OnScenarioStart {context} { startApplication "addressbook" } OnScenarioEnd {context} { applicationContext [currentApplicationContext] detach }
Generating extra warning log output whenever a step which mentions the word
delete
is about to get executed:
OnStepStart(function(context) { var text = context["text"]; if (text.search("delete") > -1) { test.warning("About to execute dangerous step: " + text); } });
OnStepStart sub { my %context = %{shift()}; if (index( $context{text}, 'delete') != -1) { ::test::warning("About to execute dangerous step: $context{text}"); } };
@OnStepStart def hook(context): text = context.text if text.find("delete") > -1: test.warning("About to execute dangerous step: %s" % text)
OnStepStart do |context| if context['text'].include? 'delete' Squish::Test.warning "About to execute dangerous step: #{context['text']}" end end
OnStepStart {context} { set text [$context text] if {[string first "delete" $text] > -1} { test warning "About to execute dangerous step: $text" } }
All BDD test cases start by running a regular script in your chosen
scripting language. This script is auto-generated, can be found in the same
directory as the corresponding test.feature
file, and is
called test.xy
, where xy = (pl|py|rb|js|tcl).
It looks like this:
source(findFile('scripts', 'python/bdd.py')) setupHooks('../shared/scripts/bdd_hooks.py') collectStepDefinitions('./steps', '../shared/steps') def main(): testSettings.throwOnFailure = True runFeatureFile('test.feature')
source(findFile('scripts', 'javascript/bdd.js')); setupHooks(['../shared/scripts/bdd_hooks.js']); collectStepDefinitions(['./steps', '../shared/steps']); function main() { testSettings.throwOnFailure = true; runFeatureFile("test.feature"); }
use warnings; use strict; use Squish::BDD; setupHooks("../shared/scripts/bdd_hooks.pl"); collectStepDefinitions("./steps", "../shared/steps"); sub main { testSettings->throwOnFailure(1); runFeatureFile("test.feature"); }
require 'squish' require 'squish/bdd' include Squish::BDD setupHooks "../shared/scripts/bdd_hooks.rb" collectStepDefinitions "./steps", "../shared/steps" def main Squish::TestSettings.throwOnFailure = true Squish::runFeatureFile "test.feature" end
source [findFile "scripts" "tcl/bdd.tcl"] Squish::BDD::setupHooks "../shared/scripts/bdd_hooks.tcl" Squish::BDD::collectStepDefinitions "./steps" "../shared/steps" proc main {} { testSettings set throwOnFailure true runFeatureFile "test.feature" }
There are some functions that are called from this script which are part of the Squish BDD API, but are rarely used in regular testcases. This section will explain what those functions do, in case you wish to reuse BDD steps or other BDD features in script-based tests.
First, setupHooks()
is called at the start of each BDD
test case to scan for and set up the hook functions described in Performing Actions During Test Execution Via Hooks (Section 6.19.10).
Next, collectStepDefinitions()
is used to scan for and
import step definitions found in the directories specified as arguments to
the function. One or more arguments can be provided, and the earlier ones
have priority over the later ones. Step implementations are typically located
in files called steps.xy
, where xy = (pl|py|rb|js|tcl).
collectStepDefinitions('./steps', '../shared/steps')
collectStepDefinitions('./steps', '../shared/steps');
use Squish::BDD; collectStepDefinitions("./steps", "../shared/steps");
include Squish::BDD collectStepDefinitions "./steps", "../shared/steps"
source [findFile "scripts" "tcl/bdd.tcl"] Squish::BDD::collectStepDefinitions "./steps" "../shared/steps"
Hence in above example, the step definition from the current Test Case Resources will be used in favor of one with the same name, if it is also found in Test Suite Resources.
Finally, runFeatureFile()
can be called on a BDD
.feature
file after hooks are set up and steps are
collected, to run all the Scenarios in the specified Gherkin feature file.
Sometimes it may be more practical to store table data for Scenario Outline placeholders or the context table in an external file instead of having the table directly in the feature file. Examples for this are large data sets or use of the same data set in multiple BDD test cases.
In order to read a table from an external file, we use the From
keyword
followed by the file path relative to the .feature
file.
For example:
Scenario: Register orders Given orderbook is running And orderbook is empty When I register orders From testdata/order_list.txt Then orderbook is not empty
In this case, we store the order_list.txt
file in the testdata
directory next to our feature file. It contains a table in the familiar Gherkin format.
# testdata/order_list.txt | name | date | type | amount | | Amy | 04.02.2017 | apple | 2 | | Josh | 04.02.2017 | peach | 3 | | Marc | 05.02.2017 | cheese | 14 | | Lea | 07.02.2017 | soda | 1 |
Squish accepts table files in the following formats:
Raw text (.txt
or no extension)
Comma-separated values (.csv
)
Tab-separated values (.tsv
)
Microsoft Excel Sheet (.xls
)
When reading tables from raw text files (.txt
or no extension), Squish simply jumps into
the file and reads it just like it was written inline in the feature file (thus we could also use further From
statements inside of a text table file). For all other formats, Squish parses the records directly.
From
uses the header (first row) in the table file to map the table columns to script
variables or Scenario Outline placeholders by default, but it is possible to define a custom header to be
used instead before reading the table file:
| customer | date | item | count | From testdata/order_list.txt
This makes it easier to use table files with our test scripts.
![]() | |
Remember that the column count of the header still has to match the column count of the table! |