BDD Test Cases in Squish: Clean Up After Your Scenarios!

BDD Test Cases in Squish: Clean Up After Your Scenarios!

Squish allows creating functional tests for user interfaces in a behavior-driven (BDD) style. Instead of expressing how a user interacts with an application (“enter this, click there”), behavior-driven tests allow using a higher degree of abstraction, enabling us to concentrate on what the user does (“enter user name, confirm login dialog”). By using the expressiveness of the scripting languages supported, a very elegant scheme for tearing down BDD test cases can be implemented.

BDD Tests In Practice

Let’s consider a sample test file in the Gherkin format (typically called
test.feature in Squish test cases) which intends to describe the desired behavior of a feature for adding entries to an address book application:

Feature: Filling of addressbook
    As a user I want to be able to fill the addressbook with 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
        And I save the address book to the file 'Addresses.txt'
        Then the file 'Addresses.txt' should contain one line

This description provides a very generic description of how the functionality for filling the address book application with entries works. In particular, it’s evident that data can be added, and the data can be saved to a file. However, the description is deliberately imprecise as to how exactly the steps are to be performed – which menu items, toolbar buttons and input fields the user needs to interact with.

Of course, this test case is not executable as-is. Instead, script code is required which defines which user actions to perform for each of the steps defined in the above description. When using JavaScript, this script code (typically saved in the path shared/steps/steps.js of a test suite) might look as follows1:

import * as names from 'names.js';

Given("addressbook application is running", function(context) {
    startApplication("addressbook");
});

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

When("I add a new person 'John','Doe','john@m.com','500600700' to address book", function(context) {
    mouseClick(waitForObject(names.addEntryButton));
    type(waitForObject(names.firstNameInput), "John");
    type(waitForObject(names.lastNameInput), "Doe");
    type(waitForObject(names.emailInput), "john@m.com");
    type(waitForObject(names.phoneInput), "500600700");
    mouseClick(waitForObject(names.addEntryOkButton));
});

When("I save the address book to the file 'Addresses.txt'", function(context) {
    mouseClick(waitForObject(names.fileMenuItem));
    mouseClick(waitForObject(names.saveAsMenuItem));
    type(waitForObject(names.fileNameInput), "Addresses.txt");
    mouseClick(waitForObject(names.saveOkButton));
});

Then("the file 'Addresses.txt' should contain one line", function(context) {
    var f = File.open("Addresses.txt");
    var content = f.read();
    f.close();
    test.compare(content.split("\n"), 1);
});

The addressbook application is launched, the user interfaces is automated to enter a new record and save it to a file, and finally the File API of Squish is used to count the number of lines in the generated file.

Cleaning Up After BDD Scenarios

Note how in the example scenario, there is a step

And I save the address book to the file ‘Addresses.txt’

The script code for this step automated the user interface such that a new file is stored to disk. In order to make our test case not leave old cruft behind (which might influence other test cases), it would be good if we removed the file at the end of the scenario.

This is a perfect use case for BDD hooks. BDD hooks are a feature of Squish which permit defining code to execute on certain events during the execution of a test case. For instance, we can define some special setup code to be executed before a Feature is executed. In our case, we can use hooks to define some code to execute when a Scenario ends – using an OnScenarioEnd hook2:

OnScenarioEnd(function(context) {
    File.remove("Addressbook.txt");
});

This code, usually stored in the file shared/scripts/bdd_hooks.js of a Squish test suite, will cause the Addressbook.txt file to be removed automatically after the last step of a BDD scenario has been executed.

Unexpectedly Terminated BDD Scenarios

However, consider what happens when the scenario gets terminated unexpectedly. For example, an object lookup error might occur in the ‘I add a new person’ step, causing an exception to be raised which not only stops execution of that step but in fact causes all subsequent steps to be skipped and the scenario to be terminated.

In this case, too, the OnScenarioEnd hook would be invoked. However, since the step

I save the address book to the file ‘Addressbook.txt’

is never executed, there is no file to delete – which in turn will make the File.remove() statement fail, logging another fatal error!

Cleanup After BDD Scenarios – When Needed!

What we really need is a post-scenario hook which only does the cleanup when needed, i.e. only when the step for saving the address book was actually executed.

A good way to do this is to make use of the context.userData field: it can be augmented with a list of functions to call at the very end, when the OnScenarioHook function is invoked. Initially this list is empty but at the end of the

I save the address book to the file ‘Addressbook.txt’

step the list can be extended with a new function which merely calls File.remove().

A good place for initializing the list is in an OnScenarioStart hook, which is executed just before the first step in a scenario is executed:

OnScenarioStart(function(context) {
    context.userData = {
        postScenarioHandlers: []
    };
});

The counter-part is a short loop in an OnScenarioEnd hook which invokes each registered handler:

OnScenarioEnd(function(context) {
    var handlers = context.userData['postScenarioHandlers'];
    for (var i in handlers) {
        handlers[i](context);
    }
});

This code iterates the (potentially) empty list postScenarioHandlers, invoking each registered handler. To provide some context, the context value given to the OnScenarioEnd hook is passed on to each handler (such that e.g. a handler could behave differently depending on the scenario which has ended).

Tying Up Loose Ends

All that’s missing now is to adjust the step for writing the Addressbook.txt file such that it registers an appropriate handler. This is easy enough to do in the scripting languages supported by Squish. In JavaScript, we can register an unnamed function:

When("I save the address book to the file 'Addresses.txt'", function(context) {
    mouseClick(waitForObject(names.fileMenuItem));
    mouseClick(waitForObject(names.saveAsMenuItem));
    type(waitForObject(names.fileNameInput), "Addresses.txt");
    mouseClick(waitForObject(names.saveOkButton));

    context.userData['postScenarioHandlers'].push(function() {
        File.remove('Addresses.txt');
    });
});

This provides two important benefits:

  1. The OnScenarioEnd hook no longer unconditionally removes a file. Instead, the File.remove() statement is only called when needed – when saving the file succeeded.
  2. The name of the file being needed is no longer spread over the script code: it used to be mentioned in both the step implementation as well as the OnScenarioEnd hook. With the new approach, all mentioning of the name Addresses.txt are in a single place, so when changing that step (e.g. by using BDD placeholders), only a single script function needs to be updated.

1) This script uses script-based object names to identify objects. Script-based object names are a new feature which were introduced with Squish GUI Tester 6.4 Beta.

2) Functions which take other functions as arguments or return functions as their return value are commonly called Higher-order functions.

Software engineer working at froglogic since 2005. When he's not currently at the swimming pool or busy building awesome Lego creations with his son, he can typically be found playing video games.

2 Comments

  1. Aurélien Gâteau 2 months ago

    Looks nice, but the `type(waitForObject(…` lines look wrong: the first closing parenthesis should be after the waited object, not at the end, no?

    • Author
      Frerich Raabe 2 months ago

      @Aurélien good catch, fixed now. Thanks for pointing this out!

Leave a reply

Your email address will not be published. Required fields are marked *

*