Squish for Java 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 Qt Academy if you desire some video guidance.

For this chapter we will use a simple Address Book application written in Java AWT/Swing as our AUT, in <SQUISHDIR>/examples/java/addressbook. It 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. 2 other versions are available, depending on which toolkit you want to use (AWT/Swing, SWT or JavaFX).

"The Java AWT \c {addressbook} example"

Example Test Suites

In the same directory as the AUT, you should find some example test suites beginning with suite_bdd_ and a script language as a suffix. Those will be similar to the test suites you produce by following this tutorial.

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. Here is 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. This application behavior specification can be passed to software developers to implement features, and at the same time it can be passed to software testers to implement automated tests.

Test implementation

Creating Test Suite

First, we need to create a Test Suite. Start the Squish IDE and select File > New Test Suite. Follow the New Test Suite wizard, provide a Test Suite name, choose the Java Toolkit, scripting language of your choice, and finally register Address Book application as AUT (if necessary).

Creating Test Case

Squish supports different types of Test Cases: "Script", "BDD" and "MBT". "Script" is the default. In order to create new "BDD Test Case", use the context menu by clicking on the expander next to New Script Test Case () button 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 new 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 Resources file named shared/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 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 a Scenario, press the Record () button next to the respective Scenario that is listed in the Scenarios tab of Test Case Resources.

"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 interactions with the AUT or any verification points added to the script will be recorded under the current 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 () in the Control Bar and select Properties.

"Control Bar"

As a result, the Squish IDE is put into Spy perspective which displays all Application Objects and their Properties. In the Application Objects View, select one of the main widgets that occupies the most space.

Selecting it will update the Properties View to its right side. Next, click on the checkbox in front of enabled in the Properties View. Finally, click on Save and Insert Verifications.

"Inserting Verification Point"

After this, 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 Finish Recording Step() button in the Control Bar that is located to the left of the current step.

Next, for the step I create a new addressbook, click on the New toolbar button of the AddressBook ( ) and then click Finish Recording Step().

Finally, for the step addressbook should have zero entries verify that the table containing the address entries is empty. To record this verification, click on Verify () while recording, and select Properties. In the Application Objects view, use the Object Picker () to select (not check) the table object. After that, you may need to navigate in the tree to one of its children to locate the appropriate Table-typed object (as opposed to one of its containers). Check yourself that the rowCount property is 0 from the Properties view and click Save and Insert Verifications. Finally, click on the last Finish Recording Step() button in the Control Bar.

As a result, Squish will generate something like following step definitions in the steps.* file (at Test Suites > Test Suite Resources):

@Given("addressbook application is running")
def step(context):
    startApplication("AddressBookSwing.jar")
    test.compare(waitForObjectExists(names.address_Book_JViewport).enabled, True)

@When("I create a new addressbook")
def step(context):
    clickButton(waitForObject(names.address_Book_JButton))

@Then("addressbook should have zero entries")
def step(context):
    test.compare(waitForObjectExists(names.address_Book_Unnamed_JTable).rowcount, 0)
Given("addressbook application is running", function(context) {
    startApplication("AddressBookSwing.jar");
    test.compare(waitForObjectExists(names.addressBookJViewport).enabled, true);
});

When("I create a new addressbook", function(context) {
    clickButton(waitForObject(names.addressBookJButton));
});

Then("addressbook should have zero entries", function(context) {
    test.compare(waitForObjectExists(names.addressBookUnnamedJTable).rowcount, 0);
Given("addressbook application is running", sub {
    my $context = shift;
    startApplication("AddressBookSwing.jar");
    test::compare(waitForObjectExists($Names::address_book_jviewport)->enabled, 1);
});

When("I create a new addressbook", sub {
    my $context = shift;
    clickButton(waitForObject($Names::address_book_jbutton));
});

Then("addressbook should have zero entries", sub {
    my $context = shift;
    test::compare(waitForObjectExists($Names::address_book_unnamed_jtable)->rowcount, 0);
Given("addressbook application is running") do |context|
    startApplication("AddressBookSwing.jar")
    Test.compare(waitForObjectExists(Names::Address_Book_JViewport).enabled, true)
end

When("I create a new addressbook") do |context|
    clickButton(waitForObject(Names::Address_Book_JButton))
end

Then("addressbook should have zero entries") do |context|
    Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_JTable).rowcount, 0)
Given "addressbook application is running" {context} {
    startApplication "AddressBookSwing.jar"
    test compare [property get [waitForObjectExists $names::Address_Book_JViewport] enabled] true
}

When "I create a new addressbook" {context} {
    invoke clickButton [waitForObject $names::Address_Book_JButton]
}

Then "addressbook should have zero entries" {context} {
    test compare [property get [waitForObjectExists $names::Address_Book_Unnamed_JTable] rowcount] 0

The application is started at the beginning of the first step due to the startApplication() call. At the end of each Scenario, the OnScenarioEnd hook function is called, causing detach() to be called on the application context. Because the AUT was started with startApplication(), the detach causes it to terminate (although not immediately - you may see multiple AUTs running at the same time).

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.

After creating the first BDD Test Case in your suite, a shared/scripts/hooks file is created in the correct script language. From the Squish IDE, it is located in the Scripts tab of Test Suite Resources.

@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. Gherkin supports 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: I add a new person 'John', 'Doe','john@m.com','500600700' to address book and '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. Click the Add button, enter the information for a new entry and click Ok to close the dialog.

Click on Finish Recording Step() button to move to the next step.

For the second missing step, we can record an object property verification like we did for the step addressbook should have zero entries, checking that rowcount is 1.

Now we have steps with hardcoded values. We will replace them with Gherkin parameter types.

For the add step, replace the hardcoded parameters with |word|, |word|, |any|, |any|. Note that each parameter will be passed to the step implementation function in the order of appearance in the descriptive name of the step.

@Step("I add a new person '|word|','|word|','|any|','|any|' to address book")
def step(context, forename, lastname, email, phone):
    clickButton(waitForObject(names.address_Book_Unnamed_JButton))
    type(waitForObject(names.address_Book_Add_Forename_JTextField), forename)
    type(waitForObject(names.address_Book_Add_Surname_JTextField), lastname)
    type(waitForObject(names.address_Book_Add_Email_JTextField), email)
    type(waitForObject(names.address_Book_Add_Phone_JTextField), phone)
    clickButton(waitForObject(names.address_Book_Add_OK_JButton))
Step("I add a new person '|word|','|word|','|any|','|any|' to address book",
    function(context, forename, lastname, email, phone) {
        clickButton(waitForObject(names.addressBookUnnamedJButton));
        mouseClick(waitForObject(names.addressBookAddForenameJTextField), 31, 14, 0, Button.Button1);
        type(waitForObject(names.addressBookAddForenameJTextField), forename);
        type(waitForObject(names.addressBookAddSurnameJTextField), lastname);
        type(waitForObject(names.addressBookAddEmailJTextField), email);
        type(waitForObject(names.addressBookAddPhoneJTextField), phone);
        clickButton(waitForObject(names.addressBookAddOKJButton));
Step("I add a new person '|word|','|word|','|any|','|any|' to address book", sub {
    my ($context, $forename, $lastname, $email, $phone) = (@_);
    clickButton(waitForObject($Names::address_book_unnamed_jbutton));
    mouseClick(waitForObject($Names::address_book_add_forename_jtextfield), 45, 13, 0, Button->Button1);
    type(waitForObject($Names::address_book_add_forename_jtextfield), $forename);
    type(waitForObject($Names::address_book_add_surname_jtextfield), $lastname);
    type(waitForObject($Names::address_book_add_email_jtextfield), $email);
    type(waitForObject($Names::address_book_add_phone_jtextfield), $phone);
    clickButton(waitForObject($Names::address_book_add_ok_jbutton));
Step("I add a new person '|word|','|word|','|any|','|any|' to address book") do |context, forename, lastname, email, phone|
    clickButton(waitForObject(Names::Address_Book_Unnamed_JButton))
    mouseClick(waitForObject(Names::Address_Book_Add_Forename_JTextField), 50, 8, 0, Button::BUTTON1)
    type(waitForObject(Names::Address_Book_Add_Forename_JTextField), forename)
    type(waitForObject(Names::Address_Book_Add_Surname_JTextField), lastname)
    type(waitForObject(Names::Address_Book_Add_Email_JTextField), email)
    type(waitForObject(Names::Address_Book_Add_Phone_JTextField), phone)
    clickButton(waitForObject(Names::Address_Book_Add_OK_JButton))
Step "I add a new person '|word|','|word|','|any|','|any|' to address book" {context forename surname email phone} {
    invoke clickButton [waitForObject $names::Address_Book_Unnamed_JButton]
    invoke mouseClick [waitForObject $names::Address_Book_Add_Forename_JTextField] 39 15 0 [enum Button Button1]
    invoke type [waitForObject $names::Address_Book_Add_Forename_JTextField] $forename
    invoke type [waitForObject $names::Address_Book_Add_Surname_JTextField] $surname
    invoke type [waitForObject $names::Address_Book_Add_Email_JTextField] $email
    invoke type [waitForObject $names::Address_Book_Add_Phone_JTextField] $phone
    invoke clickButton [waitForObject $names::Address_Book_Add_OK_JButton]

Next, we will replace the hardcoded value in the second step, so we can verify any number of entries.

@Then("'|integer|' entries should be present")
def step(context, count):
    test.compare(waitForObjectExists(names.address_Book_Unnamed_JTable).rowcount, count)
Then("'|integer|' entries should be present", function(context, count) {
        test.compare(waitForObjectExists(names.addressBookUnnamedJTable).rowcount, count);
Then("'|integer|' entries should be present", sub {
    my $context = shift;
    my $count = shift;
    test::compare(waitForObjectExists($Names::address_book_unnamed_jtable)->rowcount, $count);
Then("'|integer|' entries should be present") do |context, count|
    Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_JTable).rowcount, count)
Then "'|integer|' entries should be present" {context count} {
    test compare [property get [waitForObjectExists $names::Address_Book_Unnamed_JTable] rowcount] $count

Provide parameters for Step in table

The next Scenario will test adding multiple entries to the address book. We could use step I add a new person John','Doe','john@m.com','500600700' to address book multiple times just with different data. Instead, let's define a new step called I add new persons 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

Instead of recording it, we write the step implementation by hand. It makes use of a special variable, context.table which contains the table data.

@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, lastname, email, phone) in table:
        clickButton(waitForObject(names.address_Book_Unnamed_JButton))
        type(waitForObject(names.address_Book_Add_Forename_JTextField), forename)
        type(waitForObject(names.address_Book_Add_Surname_JTextField), lastname)
        type(waitForObject(names.address_Book_Add_Email_JTextField), email)
        type(waitForObject(names.address_Book_Add_Phone_JTextField), phone)
        clickButton(waitForObject(names.address_Book_Add_OK_JButton))
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];
        clickButton(waitForObject(names.addressBookUnnamedJButton));
        type(waitForObject(names.addressBookAddForenameJTextField), row[0]);
        type(waitForObject(names.addressBookAddSurnameJTextField), row[1]);
        type(waitForObject(names.addressBookAddEmailJTextField), row[2]);
        type(waitForObject(names.addressBookAddPhoneJTextField), row[3]);
        clickButton(waitForObject(names.addressBookAddOKJButton));
    }
});
Step("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, $lastname, $email, $phone) = @{$row};
        clickButton(waitForObject($Names::address_book_unnamed_jbutton));
        type(waitForObject($Names::address_book_add_forename_jtextfield), $forename);
        type(waitForObject($Names::address_book_add_surname_jtextfield), $lastname);
        type(waitForObject($Names::address_book_add_email_jtextfield), $email);
        type(waitForObject($Names::address_book_add_phone_jtextfield), $phone);
        clickButton(waitForObject($Names::address_book_add_ok_jbutton));
    }
});
When("I add new persons to address book") do |context|
    table = context.table
    # Drop initial row with column headers
    table.shift
    for forename, lastname, email, phone in table do
        clickButton(waitForObject(Names::Address_Book_Unnamed_JButton))
        type(waitForObject(Names::Address_Book_Add_Forename_JTextField), forename)
        type(waitForObject(Names::Address_Book_Add_Surname_JTextField), lastname)
        type(waitForObject(Names::Address_Book_Add_Email_JTextField), email)
        type(waitForObject(Names::Address_Book_Add_Phone_JTextField), phone)
        clickButton(waitForObject(Names::Address_Book_Add_OK_JButton))
    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 clickButton [waitForObject $names::Address_Book_Unnamed_JButton]
        invoke type [waitForObject $names::Address_Book_Add_Forename_JTextField] $forename
        invoke type [waitForObject $names::Address_Book_Add_Surname_JTextField] $surname
        invoke type [waitForObject $names::Address_Book_Add_Email_JTextField] $email
        invoke type [waitForObject $names::Address_Book_Add_Phone_JTextField] $phone
        invoke clickButton [waitForObject $names::Address_Book_Add_OK_JButton]
    }
}

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, context.userData can be used. We modified our existing I add a new person step to store the values we want to save into a dictionary.

@Step("I add a new person '|word|','|word|','|any|','|any|' to address book")
def step(context, forename, lastname, email, phone):
    clickButton(waitForObject(names.address_Book_Unnamed_JButton))
    type(waitForObject(names.address_Book_Add_Forename_JTextField), forename)
    type(waitForObject(names.address_Book_Add_Surname_JTextField), lastname)
    type(waitForObject(names.address_Book_Add_Email_JTextField), email)
    type(waitForObject(names.address_Book_Add_Phone_JTextField), phone)
    clickButton(waitForObject(names.address_Book_Add_OK_JButton))
    context.userData = {}
    context.userData['forename'] = forename
    context.userData['lastname'] = lastname
Step("I add a new person '|word|','|word|','|any|','|any|' to address book") do |context, forename, lastname, email, phone|
    clickButton(waitForObject(Names::Address_Book_Unnamed_JButton))
    mouseClick(waitForObject(Names::Address_Book_Add_Forename_JTextField), 50, 8, 0, Button::BUTTON1)
    type(waitForObject(Names::Address_Book_Add_Forename_JTextField), forename)
    type(waitForObject(Names::Address_Book_Add_Surname_JTextField), lastname)
    type(waitForObject(Names::Address_Book_Add_Email_JTextField), email)
    type(waitForObject(Names::Address_Book_Add_Phone_JTextField), phone)
    context.userData = Hash.new
    context.userData[:forename] = forename
    context.userData[:surname] = lastname
    clickButton(waitForObject(Names::Address_Book_Add_OK_JButton))
end
Step("I add a new person '|word|','|word|','|any|','|any|' to address book",
    function(context, forename, lastname, email, phone) {
        clickButton(waitForObject(names.addressBookUnnamedJButton));
        mouseClick(waitForObject(names.addressBookAddForenameJTextField), 31, 14, 0, Button.Button1);
        type(waitForObject(names.addressBookAddForenameJTextField), forename);
        type(waitForObject(names.addressBookAddSurnameJTextField), lastname);
        type(waitForObject(names.addressBookAddEmailJTextField), email);
        type(waitForObject(names.addressBookAddPhoneJTextField), phone);
        context.userData['forename'] = forename;
        context.userData['lastname'] = lastname;
        clickButton(waitForObject(names.addressBookAddOKJButton));
    });
Step("I add a new person '|word|','|word|','|any|','|any|' to address book", sub {
    my ($context, $forename, $lastname, $email, $phone) = (@_);
    clickButton(waitForObject($Names::address_book_unnamed_jbutton));
    mouseClick(waitForObject($Names::address_book_add_forename_jtextfield), 45, 13, 0, Button->Button1);
    type(waitForObject($Names::address_book_add_forename_jtextfield), $forename);
    type(waitForObject($Names::address_book_add_surname_jtextfield), $lastname);
    type(waitForObject($Names::address_book_add_email_jtextfield), $email);
    type(waitForObject($Names::address_book_add_phone_jtextfield), $phone);
    clickButton(waitForObject($Names::address_book_add_ok_jbutton));
    $context->{userData}{'forename'} = $forename;
    $context->{userData}{'lastname'} = $lastname;
});
Step "I add a new person '|word|','|word|','|any|','|any|' to address book" {context forename surname email phone} {
    invoke clickButton [waitForObject $names::Address_Book_Unnamed_JButton]
    invoke mouseClick [waitForObject $names::Address_Book_Add_Forename_JTextField] 39 15 0 [enum Button Button1]
    invoke type [waitForObject $names::Address_Book_Add_Forename_JTextField] $forename
    invoke type [waitForObject $names::Address_Book_Add_Surname_JTextField] $surname
    invoke type [waitForObject $names::Address_Book_Add_Email_JTextField] $email
    invoke type [waitForObject $names::Address_Book_Add_Phone_JTextField] $phone
    $context userData [dict create forename $forename surname $surname]
    invoke clickButton [waitForObject $names::Address_Book_Add_OK_JButton]
}

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_JTable, '0/0').text, context.userData['forename'], "forename")
    test.compare(waitForObjectItem(names.address_Book_Unnamed_JTable, '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_JTable, '0/0').text, context.userData[:forename], "forename?");
    Test.compare(waitForObjectItem(Names::Address_Book_Unnamed_JTable, '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.addressBookUnnamedJTable, '0/0').text, context.userData['forename'], "forename?");
    test.compare(waitForObjectItem(names.addressBookUnnamedJTable, '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_jtable, '0/0')->text, $context->{userData}{'forename'}, "forename?");
    test::compare(waitForObjectItem($Names::address_book_unnamed_jtable, '0/1')->text, $context->{userData}{'lastname'}, "lastname?");
});
Then "previously entered forename and surname shall be at the top" {context} {
    test compare [property get [waitForObjectItem $names::Address_Book_Unnamed_JTable "0/0"] text] [dict get [$context userData] forename]
    test compare [property get [waitForObjectItem $names::Address_Book_Unnamed_JTable "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 Run Test Case () button next to the respective Scenario in the Scenarios tab of 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 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"

When the breakpoint is reached, you can inspect all application objects and their properties. If the breakpoint is in a step definition or hook function, then you can add Verification Points or record code snippets there.

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 aimed at 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 add new tests with the BDD approach. The second section describes how to convert existing suites to BDD.

Extend existing tests to BDD

The first option is to keep existing Squish tests and extend them by adding new BDD tests. It's possible to have a Test Suite containing both script-based Test Cases and BDD feature files. Simply open an existing Test Suite and choose New BDD Test Case option from the drop down list.

"Creating new BDD Test Case"

Assuming your existing script-based tests make use of a library and you are calling shared functions to interact with the AUT, those functions can also be used from BDD step implementations. In the example below, a function is used from multiple script-based Test Cases:

def createNewAddressBook():
    clickButton(waitForObject(names.new_ToolbarItem))
function createNewAddressBook(){
    clickButton(waitForObject(names.newToolbarItem));
}
sub createNewAddressBook{
    clickButton(waitForObject($Names::new_toolbaritem));
}
def createNewAddressBook
    clickButton(waitForObject(Names::New_ToolbarItem))
end
proc createNewAddressBook {} {
    invoke clickButton [waitForObject $names::New_ToolbarItem]
}

New BDD Test Cases 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 Test Cases into behavior driven tests. Since a Test Suite can contain script-based 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-based Test Case will be transformed into a Scenario, which is a part of a Feature. For example, assume we have 5 script-based Test Cases. After review, we realize that those script-based Test Cases 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 script-based Squish tests that are planned to be migrated to BDD. Next, create a new Test Case by choosing New BDD Test Case option from the context menu to the right of New Script Test Case (). Each BDD Test Case corresponds to a test.feature file that contains one Feature, with one or more Scenarios. 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, decide which actions and verifications performed in the script-based Test Case should be migrated to steps. This is how an example Test Case for the addressbook application could look like:

def main():
    startApplication("Addressbook")
    test.log("Create new addressbook")
    clickButton(waitForObject(names.new_ToolbarItem))
    test.compare(waitForObjectExists(names.address_Book_Unnamed_Table).rowCount, 0)
function main() {
    startApplication("Addressbook");
    test.log("Create new addressbook");
    clickButton(waitForObject(names.newToolbarItem));
    test.compare(waitForObjectExists(names.addressBookUnnamedTable).rowCount, 0);
}
sub main {
    startApplication("Addressbook");
    test::log("Create new addressbook");
    clickButton(waitForObject($Names::new_toolbaritem));
    test::compare(waitForObjectExists($Names::address_book_unnamed_table)->rowCount, 0);
}
def main
    startApplication("Addressbook")
    Test.log("Create new addressbook")
    clickButton(waitForObject(Names::New_ToolbarItem))
    Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_Table).rowCount, 0)
end
proc main {} {
    startApplication "Addressbook"
    test log "Create new addressbook"
    invoke clickButton [waitForObject $names::New_ToolbarItem]
    test compare [property get [waitForObjectExists $names::Address_Book_Unnamed_Table] rowCount] 0
}

After analyzing the above script-based Test Case we can create the following Scenario and add it to the test.feature file:

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 step 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-based Test Case into respective step definitions and remove the lines containing test.warning. If your script-based Test Cases makes use of shared scripts, you can call those functions from the step definitions as well. For example, the final result could look like this:

@Given("addressbook application is running")
def step(context):
    startApplication('"' + os.getenv("SQUISH_PREFIX") + '\\examples\\win\\Addressbook\\Addressbook.exe"')
    test.compare(waitForObjectExists(names.address_Book_Unnamed_Window).enabled, True)

@When("I create a new addressbook")
def step(context):
    mouseClick(waitForObject(names.new_ToolbarItem))

@Then("addressbook should have zero entries")
def step(context):
    test.compare(waitForObjectExists(names.address_Book_Unnamed_Table).rowCount, 0)
Given("addressbook application is running", function(context) {
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '\\examples\\win\\Addressbook\\Addressbook.exe"');
    test.compare(waitForObjectExists(names.addressBookUnnamedWindow).enabled, true);
});

When("I create a new addressbook", function(context) {
    mouseClick(waitForObject(names.newToolbarItem));
});

Then("addressbook should have zero entries", function(context) {
    test.compare(waitForObjectExists(names.addressBookUnnamedTable).rowCount, 0);
});
Given("addressbook application is running", sub {
    my $context = shift;
    startApplication("\"$ENV{'SQUISH_PREFIX'}\\examples\\win\\Addressbook\\Addressbook.exe\"");
    test::compare(waitForObjectExists($Names::address_book_unnamed_window)->enabled, 1);
});

When("I create a new addressbook", sub {
    my $context = shift;
    clickButton(waitForObject($Names::new_toolbaritem));
});

Then("addressbook should have zero entries", sub {
    my $context = shift;
    test::compare(waitForObjectExists($Names::address_book_unnamed_table)->rowCount, 0);
});
Given("addressbook application is running") do |context|
    startApplication("\"#{ENV['SQUISH_PREFIX']}\\examples\\win\\Addressbook\\Addressbook.exe\"")
    Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_Window).enabled, true)
end

When("I create a new addressbook") do |context|
    clickButton(waitForObject(Names::New_ToolbarItem))
end

Then("addressbook should have zero entries") do |context|
    Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_Table).rowCount, 0)
end
Given "addressbook application is running" {context} {
    startApplication "\"$::env(SQUISH_PREFIX)\\examples\\win\\Addressbook\\Addressbook.exe\""
    test compare [property get [waitForObjectExists  $names::Address_Book_Unnamed_Window] enabled] true
}

When "I create a new addressbook" {context} {
    invoke mouseClick [waitForObject $names::New_ToolbarItem]
    invoke mouseClick [waitForObject $names::New_ToolbarItem]
}

Then "addressbook should have zero entries" {context} {
    test compare [property get [waitForObjectExists $names::Address_Book_Unnamed_Table] rowCount] 0
}

Note that the test.log("Create new addressbook") was removed while migrating this script-based Test to BDD. Each step name is logged when executed, so it was redundant.

These test scripts could be shorter if we moved our common code into a shared location. Each script language has its own mechanism for creating and importing libraries, but in addition, Squish has Shared Scripts and Global Scripts that you can use to locate and access shared code.

Learning More

We have now completed the tutorial. Squish can do much more than we have shown here, but the aim has been to get you started with basic testing as quickly and easily as possible. The How to Create Test Scripts, and How to Test Applications - Specifics sections provide many more examples, including those that show how tests can interact with particular input elements, such as selects, select-ones, texts, and text-areas.

The API Reference and Tools Reference give full details of Squish's testing API and the numerous functions it offers to make testing as easy and efficient as possible. The time you invested will be repaid because you'll know what functionality Squish provides out of the box and can avoid reinventing things that are already available.

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

Search Results