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.

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 \c {addressbook} example"

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.

"BDD process"

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 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 Squish IDE will remember your choice and the "BDD Test Case" will become the default when clicking on the button in the future.

"Creating new BDD Test Case"

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.

"Record Scenario"

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.

"Control Bar"

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 Save and Insert Verifications. The Squish IDE disappears and the Control Bar is shown again.

"Inserting Verification Point"

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).

"Up and Picker tool Buttons"

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");
    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.

@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 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 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 [findObject $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 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 Play button in the Test Suites view.

"Execute all Scenarios from Feature"

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.

"Execute one Scenario from Feature"

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.

"Execution results in Feature file"

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).

"Breakpoint in Feature file"

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.

"Creating new BDD Test Case"

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.

"Conversion Chart"

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 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.

© 2023 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.