Squish for JavaFX BDD Tutorials
Tutorial: Designing Behavior Driven Development (BDD) Tests
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.
Note: There is a 40-minute Online course about Behavior Driven Testing in Squish at the if you desire some video guidance.
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.
Introduction to Behavior Driven Development
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 squishide
support.
Gherkin syntax
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.
Test implementation
Creating Test Suite
First, we need to create a Test Suite, which is a container for all Test Cases. Start the squishide and select File > New Test Suite. Follow the New Test Suite wizard, provide a Test Suite name, choose the Java Toolkit and scripting language of your choice and finally register the addressbook application as AUT. See Creating a Test Suite for more details about creating new Test Suites.
Creating Test Case
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 New Script Test Case ( ) and choosing the option New BDD Test Case. The squishide
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.
Recording Step implementation
In order to record the Scenario
, click Record ( ) 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 Verify on the Control Bar and select Properties.
As a result the squishide
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 Save and Insert Verifications. The squishide
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 the Finish Recording Step ( ) 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 New button on the toolbar of the AddressBook application. Again, click Finish Recording Step ( ) 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 Verify on the Squish control bar, select Properties. 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 item's treenode in the Properties view, and you will see the boolean empty property under that. Check that, and then press Save and Insert Verifications. Finally, click the last Finish Recording Step ( ) arrow button to finish recording. Squish generates 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"); my $stage = waitForObjectExists($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} { test compare [property get [property get [waitForObjectExists $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.
@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 } }
Step parametrization
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 squishide
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 Record ( ) 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 Finish Recording Step ( ) 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))
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 [waitForObjectExists $names::Address_Book_Unnamed_itemTbl_table_view] items] length] $count
Provide parameters for Step in table
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] } }
Sharing data between Steps and Scenarios
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] }
Scenario Outline
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
.
Test execution
In the squishide
, 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 Play 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 Run Scenario. An alternative approach is to click on the Play 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.
Test debugging
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 the 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.
Re-using Step definitions
BDD test maintainability can be increased by reusing step definitions in test cases located in another directory. For more information, see collectStepDefinitions().
Tutorial: Migration of existing tests to BDD
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.
Extend existing tests to BDD
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 New BDD Test Case 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
}
Convert existing tests to BDD
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 squishide
that contains Squish tests that need to be migrated to BDD. Next, create a New Test Case by choosing New BDD Test Case 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 Create Missing Step Implementations 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 in API Reference.
© 2024 The Qt Company Ltd.
Documentation contributions included herein are the copyrights of
their respective owners.
The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation.
Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property
of their respective owners.