Table of Contents
Table of Contents
This tutorial will show you how to create, run, and modify Behavior Driven Development (BDD) tests for an example application. You will learn about Squish's most frequently used features. By the end of the tutorial you will be able to write your own tests for your own applications.
For this chapter we will use a simple Address Book application written in JavaFX as our Application Under Test (AUT). This is a very basic application that allows users to load an existing address book or create a new one, add, edit, and remove entries. The screenshot shows the application in action with a user adding a new name and address.
The JavaFX addressbook
example.
Behavior-Driven Development (BDD) is an extension of the Test-Driven Development approach which puts the definition of acceptance criteria at the beginning of the development process as opposed to writing tests after the software has been developed. With possible cycles of code changes done after testing.
Behavior Driven Tests are built out of a set of Feature
files, which
describe product features through the expected application behavior in one or many
Scenarios
. Each Scenario
is built
out of a sequence of steps which represent actions or
verifications that need to be tested for that Scenario
.
BDD focuses on expected application behavior, not on implementation details. Therefore BDD tests are described in a human-readable Domain Specific Language (DSL). As this language is not technical, such tests can be created not only by programmers, but also by product owners, testers or business analysts. Additionally, during the product development, such tests serve as living product documentation. For Squish usage, BDD tests shall be created using Gherkin syntax. The previously written product specification (BDD tests) can be turned into executable tests. This step by step tutorial presents automating BDD tests with Squish IDE support.
Gherkin files describe product features through the expected application behavior in one or many Scenarios. An example showing the "Filling of addressbook" feature of the addressbook example application.
Feature: Filling of addressbook As a user I want to fill the addressbook with entries Scenario: Initial state of created address book Given addressbook application is running When I create a new addressbook Then addressbook should have zero entries Scenario: State after adding one entry Given addressbook application is running When I create a new addressbook And I add a new person 'John','Doe','john@m.com','500600700' to address book Then '1' entries should be present Scenario: State after adding two entries Given addressbook application is running When I create a new addressbook And I add new persons to address book | forename | surname | email | phone | | John | Smith | john@m.com | 123123 | | Alice | Thomson | alice@m.com | 234234 | Then '2' entries should be present Scenario: Forename and surname is added to table Given addressbook application is running When I create a new addressbook When I add a new person 'Bob','Doe','Bob@m.com','123321231' to address book Then previously entered forename and surname shall be at the top
Most of the above is free form text (does not have to be English). It's
just the Feature
/Scenario
structure
and the leading keywords like "Given", "And", "When" and "Then" that are
fixed. Each of those keywords marks a step defining preconditions, user
actions and expected results. Above application behavior description can
be passed to software developers to implement these features and at the
same time the same description can be passed to software testers to
implement automated tests.
First, we need to create a Test Suite, which is a container for all Test Cases. Start the squishide and select | . Please follow the New Test Suite wizard, provide a Test Suite name, choose the Java Toolkit and scripting language of your choice and finally register addressbook application as AUT. Please refer to Creating a Test Suite (Section 4.4.1.2) for more details about creating new Test Suites.
Squish offers two types of Test Cases: "Script Test Case" and
"BDD Test Case". As "Script Test Case" is the default one, in order to
create new "BDD Test Case" we need to use the context menu by clicking
on the expander next to () and choosing the option
. The Squish IDE will remember your
choice and the "BDD Test Case" will become the default when clicking on
the button in the future.
The newly created BDD Test Case consists of a test.feature
file (filled with a Gherkin template while creating a new BDD test case), a file
named test.(py|js|pl|rb|tcl)
which will drive the execution
(there is no need to edit this file), and a Test Suite Resource file named
steps/steps.(py|js|pl|rb|tcl)
where step
implementation code will be placed.
We need to replace the Gherkin template with a Feature
for the addressbook example application. To do this, copy the Feature
description below and paste it into the Feature
file.
Feature: Filling of addressbook As a user I want to fill the addressbook with entries Scenario: Initial state of created address book Given addressbook application is running When I create a new addressbook Then addressbook should have zero entries
When editing the test.feature
file, a Feature
file
warning, No implementation found is displayed for each
undefined step. The implementations are in the
steps
subdirectory, in Test Case Resources,
or in Test Suite Resources. Running our
Feature
test now will currently fail at the first step with a
No Matching Step Definition and the following
steps will be skipped.
In order to record the Scenario
, press the
() next to the respective
Scenario
that is listed in the Scenarios tab in Test Case Resources view.
This will cause Squish to run the AUT so that we can interact with it. Additionally,
the Control Bar is displayed with a list of all steps that need to be
recorded. Now all interaction with the AUT or any verification points added to
the script will be recorded under the first step Given addressbook application is
running
(which is bolded in the Step list on the Control Bar). In order to verify that
this precondition is met, we will add a Verification Point. To do this, click on
on the Control Bar and select
.
As a result the Squish IDE is put into Spy mode which displays all
Application Objects and their Properties.
Select the checkbox in front
of the property showing
in the Properties View.
Finally, click on the button . The
Squish IDE disappears and the Control Bar is shown again.
When we are done with each step, we can move to the next
undefined step (playing back the ones that were previously defined)
by clicking on the ) arrow button
in the Control Bar that is located to the left of the current step.
Next, for the step When I create a new addressbook
, click on the
button on the toolbar of the AddressBook application.
Again, click () to advance to
the next step.
Finally, for the step Then addressbook should have zero entries
verify that the table containing the address entries is empty. To record this
verification, click on
on the Squish control bar, select
. In the Application Objects
view, navigate or use the Object Picker () to select (not check) the
TableView
containing the address book entries (in our
case this table is empty).
Expand the items treenode in the Properties
view, and you will see the boolean empty property under that.
Check that, and then press .
Finally, click the last () arrow button to finish recording.
As a result of this, Squish will generate the following step
definitions:
@Given("addressbook application is running") def step(context): startApplication("AddressBook.jar") stage = waitForObject(names.address_Book_Stage) test.compare(stage.showing, True) win = ToplevelWindow.byObject(stage) win.setForeground() @When("I create a new addressbook") def step(context): mouseClick(waitForObject(names.fileNewButton_button)) @Then("addressbook should have zero entries") def step(context): test.compare(waitForObjectExists(names.address_Book_Unnamed_itemTbl_table_view).items.empty, True)
Given("addressbook application is running", function(context) { startApplication("AddressBook.jar"); var stage = waitForObject(names.addressBookStage); test.compare(stage.showing, true); var win = ToplevelWindow.byObject(stage); win.setForeground(); }); When("I create a new addressbook", function(context) { mouseClick(waitForObject(names.fileNewButtonButton)); }); Then("addressbook should have zero entries", function(context) { test.compare(waitForObjectExists(names.addressBookUnnamedItemTblTableView).items.empty, true); });
Given("addressbook application is running", sub { my $context = shift; startApplication("AddressBook.jar"); waitForObjectExists($Names::address_book_stage); my $stage = findObject($Names::address_book_stage); test::compare($stage->showing, 1); my $win = Squish::ToplevelWindow->byObject($stage); $win->setForeground; }); When("I create a new addressbook", sub { my $context = shift; mouseClick(waitForObject($Names::filenewbutton_button)); }); Then("addressbook should have zero entries", sub { my $context = shift; test::compare(waitForObjectExists($Names::address_book_unnamed_itemtbl_table_view)->items->empty, 1); });
Given("addressbook application is running") do |context| startApplication("AddressBook.jar") stage = waitForObject(Names::Address_Book_Stage) Test.compare(stage.showing, true) win = ToplevelWindow::byObject(stage) win.setForeground() end When("I create a new addressbook") do |context| mouseClick(waitForObject(Names::FileNewButton_button)) end Then("addressbook should have zero entries") do |context| Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_itemTbl_table_view).items.empty, true) end
Given "addressbook application is running" {context} { startApplication "AddressBook.jar" set stage [waitForObject $names::Address_Book_Stage] test compare [property get $stage showing] true set win [Squish::ToplevelWindow byObject $stage] $win setForeground } When "I create a new addressbook" {context} { invoke mouseClick [waitForObject $names::fileNewButton_button] } Then "addressbook should have zero entries" {context} { waitForObjectExists $names::Address_Book_Unnamed_itemTbl_table_view test compare [property get [property get [findObject $names::Address_Book_Unnamed_itemTbl_table_view] items] empty] true } -->
The application is automatically started at the beginning of the first step due
to the recorded startApplication()
call. At the end of each
Scenario, the onScenarioEnd
hook is called, causing
detach()
to be called on the application context.
Because the AUT was started with startApplication()
,
this causes it to terminate.
This hook function is found in the file bdd_hooks.(py|js|pl|rb|tcl)
,
which is located in the Scripts tab of the Test
Suite Resources view. You can define additional hook functions here.
For a list of all available hooks, please refer to Performing Actions During Test Execution Via Hooks (Section 6.19.10).
@OnScenarioEnd def hook(context): for ctx in applicationContextList(): ctx.detach()
OnScenarioEnd(function(context) { applicationContextList().forEach(function(ctx) { ctx.detach(); }); });
OnScenarioEnd(sub { foreach (applicationContextList()) { $_->detach(); } });
OnScenarioEnd do |context| applicationContextList().each { |ctx| ctx.detach() } end
OnScenarioEnd { context } { foreach ctx [applicationContextList] { applicationContext $ctx detach } }
So far, our steps did not use any parameters and all values were
hardcoded. Squish has different types of parameters like
any
, integer
or word
, allowing our
step definitions to be more reusable. Let us add a
new Scenario
to our Feature
file which will provide
step parameters for both the Test Data and the expected results.
Copy the below section into your Feature file.
Scenario: State after adding one entry Given addressbook application is running When I create a new addressbook And I add a new person 'John','Doe','john@m.com','500600700' to address book Then '1' entries should be present
After saving
the Feature
file, the Squish IDE provides a hint that only 2
steps need to be implemented: When I add a new person 'John',
'Doe','john@m.com','500600700' to address book
and Then '1' entries should
be present
. The remaining steps already have a matching
step implementation.
To record the missing steps, hit ) next to the
test case name in the Test Suites view. The script will
play until it gets to the missing step and then prompt you to implement it. If
you select the Add
button, then you can type in the information for a new entry. Click on the
(
) button to move to the next step. For the second missing
step, we could record an object property verification
like we did for the step
Then addressbook should have zero
entries
, but on items.length.
Now we parametrize the generated step implementation by replacing the values with parameter types.
Since we want to be able to add different names, replace 'John' with '|word|'.
Note that each parameter will be passed to the step implementation function in the order of
appearance in the descriptive name of the step. Finish parametrizing by editing the typed values
into keywords, to look like this example step
When I add a new person 'John', 'Doe','john@m.com','500600700'
to address book:
@When("I add a new person '|word|','|word|','|any|','|integer|' to address book") def step(context, forename, lastname, email, phone): mouseClick(waitForObject(names.editAddButton_button)) type(waitForObject(names.address_Book_Add_forenameText_text_input_text_field), forename) type(waitForObject(names.address_Book_Add_surnameText_text_input_text_field), lastname) type(waitForObject(names.address_Book_Add_emailText_text_input_text_field), email) type(waitForObject(names.address_Book_Add_phoneText_text_input_text_field), phone) mouseClick(waitForObject(names.address_Book_Add_OK_button))
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", function(context, forename, lastname, email, phone) { mouseClick(waitForObject(names.editAddButtonButton)); type(waitForObject(names.addressBookAddForenameTextTextInputTextField), forename); type(waitForObject(names.addressBookAddSurnameTextTextInputTextField), lastname); type(waitForObject(names.addressBookAddEmailTextTextInputTextField), email); type(waitForObject(names.addressBookAddPhoneTextTextInputTextField), phone); mouseClick(waitForObject(names.addressBookAddOKButton)); });
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", sub { my ($context, $forename, $surname, $email, $phone) = @_; mouseClick(waitForObject($Names::editaddbutton_button)); type(waitForObject($Names::address_book_add_forenametext_text_input_text_field), $forename); type(waitForObject($Names::address_book_add_surnametext_text_input_text_field), $surname); type(waitForObject($Names::address_book_add_emailtext_text_input_text_field), $email); type(waitForObject($Names::address_book_add_phonetext_text_input_text_field), $phone); mouseClick(waitForObject($Names::address_book_add_ok_button)); });
When("I add a new person '|word|','|word|','|any|','|integer|' to address book") do |context, forename, surname, email, phone| mouseClick(waitForObject(Names::EditAddButton_button)) type(waitForObject(Names::Address_Book_Add_forenameText_text_input_text_field), forename) type(waitForObject(Names::Address_Book_Add_surnameText_text_input_text_field), surname) type(waitForObject(Names::Address_Book_Add_emailText_text_input_text_field), email) type(waitForObject(Names::Address_Book_Add_phoneText_text_input_text_field), phone) mouseClick(waitForObject(Names::Address_Book_Add_OK_button)) end
When "I add a new person '|word|','|word|','|any|','|integer|' to address book" {context forename surname email phone} { invoke mouseClick [waitForObject $names::editAddButton_button] invoke type [waitForObject $names::Address_Book_Add_forenameText_text_input_text_field] $forename invoke type [waitForObject $names::Address_Book_Add_surnameText_text_input_text_field] $surname invoke type [waitForObject $names::Address_Book_Add_emailText_text_input_text_field] $email invoke type [waitForObject $names::Address_Book_Add_phoneText_text_input_text_field] $phone invoke mouseClick [waitForObject $names::Address_Book_Add_OK_button] }
If we recorded the final Then
as a missing step, and
verified the items.length is 1 in the tableview, we can modify the
step so that it takes a parameter, so it can verify other integer values later.
@Then("'|integer|' entries should be present") def step(context, count): test.compare(waitForObjectExists(names.address_Book_Unnamed_itemTbl_table_view).items.length, count)
Then("'|integer|' entries should be present", function(context, count) { test.compare(waitForObjectExists(names.addressBookUnnamedItemTblTableView).items.length, count);
Then("'|integer|' entries should be present", sub { my ($context, $count) = @_; test::compare(waitForObjectExists($Names::address_book_unnamed_itemtbl_table_view)->items->length, $count);
Then("'|integer|' entries should be present") do |context, count| Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_itemTbl_table_view).items.length, count)
Then "'|integer|' entries should be present" {context count} { test compare [property get [property get [findObject $names::Address_Book_Unnamed_itemTbl_table_view] items] length] $count }
The next Scenario
will test adding multiple entries to the address
book. We could use step When I add a new person John','Doe','john@m.com','500600700'
to address book
multiple times just with different data. But lets instead define a
new step called When I add a new person to address book
which will handle data from a table.
Scenario: State after adding two entries Given addressbook application is running When I create a new addressbook And I add new persons to address book | forename | surname | email | phone | | John | Smith | john@m.com | 123123 | | Alice | Thomson | alice@m.com | 234234 | Then '2' entries should be present
The step implementation to handle such tables looks like this:
@When("I add new persons to address book") def step(context): table = context.table # Drop initial row with column headers table.pop(0) for (forename, surname, email, phone) in table: mouseClick(waitForObject(names.editAddButton_button)) type(waitForObject(names.address_Book_Add_forenameText_text_input_text_field), forename) type(waitForObject(names.address_Book_Add_surnameText_text_input_text_field), surname) type(waitForObject(names.address_Book_Add_emailText_text_input_text_field), email) type(waitForObject(names.address_Book_Add_phoneText_text_input_text_field), phone) mouseClick(waitForObject(names.address_Book_Add_OK_button))
When("I add new persons to address book", function(context) { var table = context.table; for (var i = 1; i < table.length; ++i) { var row = table[i]; mouseClick(waitForObject(names.editAddButtonButton)); type(waitForObject(names.addressBookAddForenameTextTextInputTextField), row[0]); type(waitForObject(names.addressBookAddSurnameTextTextInputTextField), row[1]); type(waitForObject(names.addressBookAddEmailTextTextInputTextField), row[2]); type(waitForObject(names.addressBookAddPhoneTextTextInputTextField), row[3]); mouseClick(waitForObject(names.addressBookAddOKButton)); } });
When("I add new persons to address book", sub { my $context = shift; my $table = $context->{'table'}; # Drop initial row with column headers shift(@{$table}); for my $row (@{$table}) { my ($forename, $surname, $email, $phone) = @{$row}; mouseClick(waitForObject($Names::editaddbutton_button)); type(waitForObject($Names::address_book_add_forenametext_text_input_text_field), $forename); type(waitForObject($Names::address_book_add_surnametext_text_input_text_field), $surname); type(waitForObject($Names::address_book_add_emailtext_text_input_text_field), $email); type(waitForObject($Names::address_book_add_phonetext_text_input_text_field), $phone); mouseClick(waitForObject($Names::address_book_add_ok_button)); } });
When("I add new persons to address book") do |context| table = context.table # Drop initial row with column headers table.shift for forename, surname, email, phone in table do mouseClick(waitForObject(Names::EditAddButton_button)) type(waitForObject(Names::Address_Book_Add_forenameText_text_input_text_field), forename) type(waitForObject(Names::Address_Book_Add_surnameText_text_input_text_field), surname) type(waitForObject(Names::Address_Book_Add_emailText_text_input_text_field), email) type(waitForObject(Names::Address_Book_Add_phoneText_text_input_text_field), phone) mouseClick(waitForObject(Names::Address_Book_Add_OK_button)) end end
When "I add new persons to address book" {context} { set table [$context table] # Drop initial row with column headers foreach row [lreplace $table 0 0] { foreach {forename surname email phone} $row break invoke mouseClick [waitForObject $names::editAddButton_button] invoke type [waitForObject $names::Address_Book_Add_forenameText_text_input_text_field] $forename invoke type [waitForObject $names::Address_Book_Add_surnameText_text_input_text_field] $surname invoke type [waitForObject $names::Address_Book_Add_emailText_text_input_text_field] $email invoke type [waitForObject $names::Address_Book_Add_phoneText_text_input_text_field] $phone invoke mouseClick [waitForObject $names::Address_Book_Add_OK_button] } }
Lets add a new Scenario
to the Feature
file. This
time we would like to check not the number of entries in address book list, but if this
list contains proper data. Because we enter data into the address book in one
step and verify them in another, we must share information about
entered data among those steps in order to perform a verification.
Scenario: Forename and surname is added to table Given addressbook application is running When I create a new addressbook When I add a new person 'Bob','Doe','Bob@m.com','123321231' to address book Then previously entered forename and surname shall be at the top
To share this data, the context.userData can be used.
@When("I add a new person '|word|','|word|','|any|','|integer|' to address book") def step(context, forename, lastname, email, phone): mouseClick(waitForObject(names.editAddButton_button)) type(waitForObject(names.address_Book_Add_forenameText_text_input_text_field), forename) type(waitForObject(names.address_Book_Add_surnameText_text_input_text_field), lastname) type(waitForObject(names.address_Book_Add_emailText_text_input_text_field), email) type(waitForObject(names.address_Book_Add_phoneText_text_input_text_field), phone) mouseClick(waitForObject(names.address_Book_Add_OK_button)) context.userData = {} context.userData['forename'] = forename context.userData['lastname'] = lastname
When("I add a new person '|word|','|word|','|any|','|integer|' to address book") do |context, forename, surname, email, phone| mouseClick(waitForObject(Names::EditAddButton_button)) type(waitForObject(Names::Address_Book_Add_forenameText_text_input_text_field), forename) type(waitForObject(Names::Address_Book_Add_surnameText_text_input_text_field), surname) type(waitForObject(Names::Address_Book_Add_emailText_text_input_text_field), email) type(waitForObject(Names::Address_Book_Add_phoneText_text_input_text_field), phone) mouseClick(waitForObject(Names::Address_Book_Add_OK_button)) context.userData = Hash.new context.userData[:forename] = forename context.userData[:surname] = surname
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", function(context, forename, lastname, email, phone) { mouseClick(waitForObject(names.editAddButtonButton)); type(waitForObject(names.addressBookAddForenameTextTextInputTextField), forename); type(waitForObject(names.addressBookAddSurnameTextTextInputTextField), lastname); type(waitForObject(names.addressBookAddEmailTextTextInputTextField), email); type(waitForObject(names.addressBookAddPhoneTextTextInputTextField), phone); mouseClick(waitForObject(names.addressBookAddOKButton)); context.userData = {}; context.userData['forename'] = forename; context.userData['lastname'] = lastname; });
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", sub { my ($context, $forename, $surname, $email, $phone) = @_; mouseClick(waitForObject($Names::editaddbutton_button)); type(waitForObject($Names::address_book_add_forenametext_text_input_text_field), $forename); type(waitForObject($Names::address_book_add_surnametext_text_input_text_field), $surname); type(waitForObject($Names::address_book_add_emailtext_text_input_text_field), $email); type(waitForObject($Names::address_book_add_phonetext_text_input_text_field), $phone); mouseClick(waitForObject($Names::address_book_add_ok_button)); $context->{userData}{'forename'} = $forename; $context->{userData}{'surname'} = $surname; });
When "I add a new person '|word|','|word|','|any|','|integer|' to address book" {context forename surname email phone} { invoke mouseClick [waitForObject $names::editAddButton_button] invoke type [waitForObject $names::Address_Book_Add_forenameText_text_input_text_field] $forename invoke type [waitForObject $names::Address_Book_Add_surnameText_text_input_text_field] $surname invoke type [waitForObject $names::Address_Book_Add_emailText_text_input_text_field] $email invoke type [waitForObject $names::Address_Book_Add_phoneText_text_input_text_field] $phone invoke mouseClick [waitForObject $names::Address_Book_Add_OK_button] $context userData [dict create forename $forename surname $surname] }
All data stored in context.userData can be accessed in all
steps and Hooks
in all Scenarios
of the given Feature
. Finally, we need to implement
the step Then previously entered forename and lastname shall be at the top
.
@Then("previously entered forename and surname shall be at the top") def step(context): test.compare(waitForObjectItem(names.address_Book_Unnamed_itemTbl_table_view, '0/0').text, context.userData['forename'], "forename") test.compare(waitForObjectItem(names.address_Book_Unnamed_itemTbl_table_view, '0/1').text, context.userData['lastname'], "lastname")
Then("previously entered forename and surname shall be at the top") do |context| Test.compare(waitForObjectItem(Names::Address_Book_Unnamed_itemTbl_table_view, '0/0').text, context.userData[:forename], "forename"); Test.compare(waitForObjectItem(Names::Address_Book_Unnamed_itemTbl_table_view, '0/1').text, context.userData[:surname], "surname") end
Then("previously entered forename and surname shall be at the top", function(context) { test.compare(waitForObjectItem(names.addressBookUnnamedItemTblTableView, '0/0').text, context.userData['forename'], "forename"); test.compare(waitForObjectItem(names.addressBookUnnamedItemTblTableView, '0/1').text, context.userData['lastname'], "lastname"); });
Then("previously entered forename and surname shall be at the top", sub { my $context = shift; test::compare(waitForObjectItem($Names::address_book_unnamed_itemtbl_table_view, '0/0')->text, $context->{userData}{'forename'}, "forename?"); test::compare(waitForObjectItem($Names::address_book_unnamed_itemtbl_table_view, '0/1')->text, $context->{userData}{'surname'}, "surname?"); });
Then "previously entered forename and surname shall be at the top" {context} { test compare [property get [waitForObjectItem $names::Address_Book_Unnamed_itemTbl_table_view "0/0"] text] [dict get [$context userData] forename] test compare [property get [waitForObjectItem $names::Address_Book_Unnamed_itemTbl_table_view "0/1"] text] [dict get [$context userData] surname] }
Assume our Feature
contains the following two Scenarios
:
Scenario: State after adding one entry Given addressbook application is running When I create a new addressbook And I add a new person 'John','Doe','john@m.com','500600700' to address book Then '1' entries should be present Scenario: State after adding one entry Given addressbook application is running When I create a new addressbook And I add a new person 'Bob','Koo','bob@m.com','500600800' to address book Then '1' entries should be present
As we can see, those Scenarios
perform the same actions using different
test data. The same can be achieved by using a Scenario Outline
(a
Scenario
template with placeholders) and Examples (a table with
parameters).
Scenario Outline: Adding single entries multiple time Given addressbook application is running When I create a new addressbook And I add a new person '<forename>','<lastname>','<email>','<phone>' to address book Then '1' entries should be present Examples: | forename | lastname | email | phone | | John | Doe | john@m.com | 500600700 | | Bob | Koo | bob@m.com | 500600800 |
Please note that the OnScenarioEnd
hook will be executed at the end of
each loop iteration in a Scenario Outline
.
In the Squish IDE, users can execute all Scenarios
in a
Feature
, or execute only one selected Scenario
. In
order to execute all Scenarios
, the proper Test Case has to be
executed by clicking on the button in the Test Suites view.
In order to execute only one Scenario
, you need to open the
Feature
file, right-click on the given Scenario
and
choose . An alternative approach is to click on the
button next to the respective
Scenario
in the Scenarios tab in Test Case Resources.
After a Scenario
is executed, the Feature
file is
colored according to the execution results. More detailed information (like logs) can
be found in the Test Results View.
Squish offers the possibility to pause an execution of a Test Case at any point in
order to check script variables, spy application objects or run custom
code in the Squish script console. To do this, a breakpoint has to be placed before
starting the execution, either in the Feature
file at any line
containing a step or at any line of executed code (i.e. in middle of
step definition code).
After the breakpoint is reached, you can inspect all application objects and their properties. If a breakpoint is placed at a step definition or a hook is reached, then you can additionally add Verification Points or record code snippets.
BDD test maintainability can be increased by reusing step definitions in test cases located in another directory. For more information, see collectStepDefinitions().
This chapter is for users that have existing Squish tests and who would like to introduce Behavior Driven Testing. The first section describes how to keep the existing tests and just create new tests with the BDD approach. The second section describes how to convert existing Script Test Cases to BDD tests.
The first option is to keep any existing Squish Test Cases and extend them by
adding new BDD tests. It's possible to have a Test Suite
containing
both script-based and BDD Test Cases. Simply open existing
Test Suite
and choose option from drop down list.
Assuming your existing Test Cases make use of a library and you are calling shared functions to interact with the AUT, those functions can still be used in existing Script Test Cases as well as newly created BDD Test Cases. In the example below, a function is used from multiple Script Test Cases:
def createNewAddressBook(): mouseClick(waitForObject(names.fileNewButton_button))
def createNewAddressBook mouseClick(waitForObject(Names::FileNewButton_button)) end
function createNewAddressBook(){ mouseClick(waitForObject(names.fileNewButtonButton)); }
sub createNewAddressBook{ mouseClick(waitForObject($Names::filenewbutton_button)); }
proc createNewAddressBook {} { invoke mouseClick [waitForObject $names::fileNewButton_button] }
BDD step implementations can easily use the same function:
@When("I create a new addressbook") def step(context): createNewAddressBook()
When("I create a new addressbook", function(context){ createNewAddressBook() });
When("I create a new addressbook", sub { createNewAddressBook(); });
When("I create a new addressbook") do |context| createNewAddressBook end
When "I create a new addressbook" {context} { createNewAddressBook }
The second option is to convert an existing Test Suite
that
contains script-based tests into behavior driven tests. Since a Test
Suite
can Script Test Cases and BDD Test Cases, migration can be done
gradually. A Test Suite
containing a mix of both Test Case
types can be executed and results analyzed without any extra effort required.
The first step is to review all Test Cases of the existing Test Suite
and group them by the Feature
they test. Each Script Test Case will
be transformed into a Scenario
, which is a part of a
Feature
. For example, assume we have 5 Script Test Cases. After
review, we realize that they examine two Features
.
Therefore, when migration is completed, our Test Suite will contain two BDD Test Cases,
each of them containing one Feature
. Each Feature
will contain multiple Scenarios
. In our example the first
Feature
contains three Scenarios
and the second
Feature
contains two Scenarios
.
At the beginning, open a Test Suite
in the Squish IDE that contains
Squish tests that need to be migrated to BDD. Next, create a
by choosing
option from the
drop-down menu. Each BDD Test Case contains a test.feature
file
that can be filled with maximum one Feature
. Next, open the
test.feature
file to describe the Features
using the Gherkin language. Following the syntax from the template, edit the
Feature
name and optionally provide a short description. Next,
analyze which actions and verifications are performed in the Script Test Case that
are going to be migrated. This is how an example Test Case for the addressbook
application could look like:
def main(): startApplication("AddressBook.jar") test.log("Create new addressbook") mouseClick(waitForObject(names.fileNewButton_button)) test.compare(waitForObjectExists(names.address_Book_Unnamed_itemTbl_table_view).items.empty, True)
def main startApplication("AddressBook.jar") Test.log("Create new addressbook") mouseClick(waitForObject(Names::FileNewButton_button)) Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_itemTbl_table_view).items.empty, true) end
function main(){ startApplication("AddressBook.jar"); test.log("Create new addressbook"); mouseClick(waitForObject(names.fileNewButtonButton)); test.compare(waitForObjectExists(names.addressBookUnnamedItemTblTableView).items.empty, true); }
sub main { startApplication("AddressBook.jar"); test::log("Create new addressbook"); mouseClick(waitForObject($Names::filenewbutton_button)); test::compare(waitForObjectExists($Names::address_book_unnamed_itemtbl_table_view)->items->empty, 1); }
proc main {} { startApplication "AddressBook.jar" test log "Create new addressbook" invoke mouseClick [waitForObject $names::fileNewButton_button] test compare [property get [property get [waitForObjectExists $names::Address_Book_Unnamed_itemTbl_table_view] items] empty] true }
After analyzing the above Test Case we can create
the following Scenario
and add it to
test.feature
:
Scenario: Initial state of created address book Given addressbook application is running When I create a new addressbook Then addressbook should have zero entries
Next, right-click on the Scenario
and choose the option
from the context menu. This
will create a skeleton of steps definitions:
@Given("addressbook application is running") def step(context): test.warning("TODO implement addressbook application is running") @When("I create a new addressbook") def step(context): test.warning("TODO implement I create a new addressbook") @Then("addressbook should have zero entries") def step(context): test.warning("TODO implement addressbook should have zero entries")
Given("addressbook application is running", function(context) { test.warning("TODO implement addressbook application is running"); }); When("I create a new addressbook", function(context) { test.warning("TODO implement I create a new addressbook"); }); Then("addressbook should have zero entries", function(context) { test.warning("TODO implement addressbook should have zero entries"); });
Given("addressbook application is running", sub { my $context = shift; test::warning("TODO implement addressbook application is running"); }); When("I create a new addressbook", sub { my $context = shift; test::warning("TODO implement I create a new addressbook"); }); Then("addressbook should have zero entries", sub { my $context = shift; test::warning("TODO implement addressbook should have zero entries"); });
Given("addressbook application is running") do |context| Test.warning "TODO implement addressbook application is running" end When("I create a new addressbook") do |context| Test.warning "TODO implement I create a new addressbook" end Then("addressbook should have zero entries") do |context| Test.warning "TODO implement addressbook should have zero entries" end
Given "addressbook application is running" {context} { test warning "TODO implement addressbook application is running" } When "I create a new addressbook" {context} { test warning "TODO implement I create a new addressbook" } Then "addressbook should have zero entries" {context} { test warning "TODO implement addressbook should have zero entries" }
Now we put code snippets from the Script Test Case into respective
step definitions and remove the lines containing
test.warning
. If your Script Test Cases make use of shared scripts, you
can call those functions inside of the step definition as well. For
example, the final result could look like this:
@Given("addressbook application is running") def step(context): startApplication("AddressBook.jar") stage = waitForObject(names.address_Book_Stage) test.compare(stage.showing, True) @When("I create a new addressbook") def step(context): mouseClick(waitForObject(names.fileNewButton_button)) @Then("addressbook should have zero entries") def step(context): test.compare(waitForObjectExists(names.address_Book_Unnamed_itemTbl_table_view).items.empty, True)
Given("addressbook application is running") do |context| startApplication("AddressBook.jar") stage = waitForObject(Names::Address_Book_Stage) Test.compare(stage.showing, true) end When("I create a new addressbook") do |context| mouseClick(waitForObject(Names::FileNewButton_button)) end Then("addressbook should have zero entries") do |context| Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_itemTbl_table_view).items.empty, true) end
Given("addressbook application is running", function(context) { startApplication("AddressBook.jar"); var stage = waitForObject(names.addressBookStage); test.compare(stage.showing, true); }); When("I create a new addressbook", function(context) { mouseClick(waitForObject(names.fileNewButtonButton)); }); Then("addressbook should have zero entries", function(context) { test.compare(waitForObjectExists(names.addressBookUnnamedItemTblTableView).items.empty, true); });
Given("addressbook application is running", sub { my $context = shift; startApplication("AddressBook.jar"); my $stage = waitForObject($Names::address_book_stage); test::compare($stage->showing, 1); }); When("I create a new addressbook", sub { my $context = shift; mouseClick(waitForObject($Names::filenewbutton_button)); }); Then("addressbook should have zero entries", sub { my $context = shift; test::compare(waitForObjectExists($Names::address_book_unnamed_itemtbl_table_view)->items->empty, 1); });
Given "addressbook application is running" {context} { startApplication "AddressBook.jar" set stage [waitForObject $names::Address_Book_Stage] test compare [property get $stage showing] true } When "I create a new addressbook" {context} { invoke mouseClick [waitForObject $names::fileNewButton_button] } Then "addressbook should have zero entries" {context} { test compare [property get [property get [waitForObjectExists $names::Address_Book_Unnamed_itemTbl_table_view] items] empty] true }
Note that the test.log("Create new addressbook")
got
removed while migrating this script-based test to BDD.
When the step I create a new addressbook
is executed, the
step name will be logged into Test Results, so the
test.log
call would have been redundant.
The above example was simplified for this tutorial. In order to take full advantage of Behavior Driven Testing in Squish, please familiarize yourself with the section Behavior Driven Testing (Section 6.19) in API Reference Manual (Chapter 6).