Squish for Tk Tutorials

Learn how to test Tk applications.

Tutorial: Starting to Test Tk Applications

Squish comes with an IDE and command line tools. Whenever we show how to achieve something using the IDE, we will follow with an explanation of how to do the same thing using the command line tools. Using an IDE is the easiest and best way to start, but once you build up lots of tests, you will want to automate them. For example, to do nightly runs of your regression test suite. Therefore, it is worth knowing how to use the command line tools that can be run from batch files or shell scripts.

We use a simple Address Book script as our AUT. It is shipped with Squish as SQUISHDIR/examples/tk/addressbook.tcl. This is a Tcl/Tk script that allows users to load an existing address book or create a new one, add, edit, and remove entries, and save (or save as), the new or modified addressbook. Despite the application's simplicity, it has all the key features that most standard applications have: a menu bar with pull down menus, a toolbar, and a central area—in this case showing a table. It pop-up modal dialogs for adding and editing items. All the ideas and practices that you learn to test this application can easily be adapted to your own applications. And naturally, the User Guide has many more examples and shows how to test lots of Tk-specific features, as well as all the standard editing widgets.

Note: Throughout the manual, we often refer to the SQUISHDIR directory. This means the directory where Squish is installed, which might be C:\Squish, /usr/local/squish, /opt/local/squish, or something else, depending on where you installed it. The exact location doesn't matter, so long as you mentally translate the SQUISHDIR directory to whatever the directory really is when you see paths and filenames in this manual.

To execute the Tk Address Book application you will need to have a working Tcl/Tk installation on your system.

The screenshot shows the application in action with a user adding a new name and address.

"The Tk addressbook.tcl example"

Using the Examples

The first time you try running a test for one of the example AUTs, you might get a fatal error that begins Squish couldn't find the AUT to start.... To recover from the error, click the Test Suite Settings toolbar button, and in the Application Under Test (AUT) section, choose the AUT from the combobox if it is available, or click the Browse button and navigate to the AUT's executable in the finder dialog. Some versions of Squish automatically open this dialog if no AUT is specified. You need to do this only once per example AUT, and not at all when testing your own AUTs.

On Unix-based systems, the executable may be a script such as addressbook.tcl, while on Windows, it may need to be the tclsh installed by ActiveState, in which case, the Arguments should be addressbook.tcl, and the Working Directory should be the absolute path to that script.

Squish Concepts

In the following sections we will create a test suite and then create some tests, but first we will very briefly review some key Squish concepts.

To perform testing, you need:

  1. An application to test, known as the Application Under Test (AUT).
  2. A test script that exercises the AUT.

One fundamental aspect of Squish's approach is that the AUT and the test script that exercises it are always executed in two separate processes. This ensures that even if the AUT crashes, it should not crash Squish. In such cases, the test script will fail gracefully and log an error message. In addition to insulating Squish and test scripts from AUT crashes, running the AUT and the test script in separate processes brings other benefits. For example, it makes it easier to store the test scripts in a central location and to perform remote testing on different machines and platforms. The ability to do remote testing is particularly useful for testing AUTs that run on multiple platforms and for testing AUTs that run on embedded devices.

Squish runs a small server, squishserver, that handles the communication between the AUT and the test script. The test script is executed by the squishrunner tool, which in turn connects to squishserver. squishserver starts the instrumented AUT on the device, which starts the Squish hook. The hook is a small library that makes the AUT's live running objects accessible, and allows communication with squishserver. With the hook in place, squishserver can query AUT objects regarding their state and can execute commands on behalf of squishrunner. squishrunner directs the AUT to perform whatever actions the test script specifies.

All the communication takes place using network sockets which means that everything can be done on a single machine, or the test script can be executed on one machine and the AUT can be tested over the network on another machine.

The following diagram illustrates how the individual Squish tools work together.

"Squish tools"

From the test engineer's perspective, this separation is not noticeable because all the communication is handled transparently behind the scenes.

Tests can be written and executed using the Squish IDE, in which case squishserver is started and stopped automatically, and the test results are displayed in the Squish IDE's Test Results view. The following diagram illustrates what happens behind the scenes when the Squish IDE is used.

"Squish IDE"

The Squish tools can also be used from the command line without the Squish IDE. This is useful if you prefer to use you own tools, such as your favorite editor or want to perform automatic batch testing. For example, when running regression tests overnight. In these cases, squishserver must be started manually and stopped when all the testing is complete or started and stopped for each test.

Note: The Squish documentation mostly uses the term widget when referring to GUI objects, such as buttons, menus, menu items, labels, and table controls. Windows users might be more familiar with the terms control and container, but here we use the term widget for both. Similarly, macOS users may be used to the term view.

Creating a Test Suite

A test suite is a collection of one or more test cases (tests). Using a test suite is convenient since it makes it easy to share scripts and test data between a group of related tests.

Here, and throughout the tutorial, we will start by describing how to do things using the IDE, with the information for command line users following.

Creating Test Suites from Squish IDE

Start up the Squish IDE, by clicking or double-clicking the squishide icon, by launching squishide from the taskbar menu or by executing squishide on the command line, whichever you prefer and find suitable for the platform you are using. Once Squish starts up, you might be greeted with a Welcome Page. Click the Workbench button in the upper right to dismiss it. Then, the squishide will look similar to the screenshot, but probably slightly different depending on the windowing system, colors, fonts, and theme that you use.

"The Squish IDE with no Test Suites"

Click File > New Test Suite to pop-up the New Test Suite wizard shown below.

"Name & Directory page"

Enter a name for your test suite and choose the folder where you want the test suite to be stored. In the screenshot we have called the test suite suite_tcl and will put it inside the addressbook folder. (For your own tests you might use a more meaningful name such as "suite_addressbook"; we chose "suite_tcl" because the tutorial contains test suites for each scripting language that Squish supports.) Naturally, you can choose whatever name and folder you prefer. Once the details are complete, click Next to go on to the Toolkit (or Scripting Language) page.

"Toolkit page"

If you get this wizard page, click the toolkit your AUT uses. For this example, we must click Tk since we are testing a Tk application. Then click Next to go to the Scripting Language page.

"Scripting Language page"

Choose whichever scripting language you want—the only constraint is that you can only use one scripting language per test suite. Naturally, if you are a Tcl/Tk programmer, Tcl would be your choice here. Having chosen a scripting language, click Next once more to get to the wizard's last page.

"AUT page"

If you are creating a new test suite for an AUT that Squish already knows about, simply click the combobox to drop-down the list of AUTs and choose the one you want. If the combobox is empty or your AUT isn't listed, click the Browse button to the right of the combobox—this will pop-up a file open dialog from which you can choose your AUT. Once you have chosen the AUT, click Finish and Squish will create a sub-folder with the same name as the test suite, and will create a file inside that folder called suite.conf that contains the test suite's configuration details. Squish will also register the AUT with the squishserver. The wizard will then close and Squish's IDE will look similar to the screenshot below.

"The suite_tcl test suite"

We are now ready to start creating tests. Read on to learn how to create test suites without using the IDE, or skip ahead to Recording Tests and Verification Points.

Creating Test Suites from Command Line

To create a new test suite from the command line:

  1. Create a new directory to hold the test suite—the directory's name should begin with suite. For this tutorial, we have created the SQUISHDIR/examples/tk/addressbook/suite_tcl directory for Tcl tests. (We also have similar subdirectories for other languages but this is purely for the sake of example, since normally we only use one language for all our tests.)
  2. Create a plain text file (ASCII or UTF-8 encoding) called suite.conf in the suite subdirectory. This is the test suite's configuration file, and at the minimum it must identify the AUT, the scripting language used for the tests, and the wrappers (i.e., the GUI toolkit or library) that the AUT uses. The format of the file is key = value, with one key–value pair per line. For example:
    AUT      = addressbook.tcl
    LANGUAGE = Tcl
    WRAPPERS = Tk

    On Windows, it might look more like this:

    AUT      = tclsh addressbook.tcl
    CWD      = SQUISHDIR/examples/tk/addressbook
    LANGUAGE = Tcl
    WRAPPERS = Tk

    The AUT can be a Tcl script (on *nix systems), or the tclsh executable, or a batch file/shell script that starts the Tk AUT in the foreground. The LANGUAGE can be set to whichever script language you prefer. The WRAPPERS should be set to Tk.

    On Windows, since the Tcl script may not be executable, we set the AUT to be tclsh, and supply the name of the Tcl script as an argument. We must also set the CWD to the location of the addressbook.tcl AUT script.

  3. Register the AUT with the squishserver.

    Note: Each AUT must be registered with the squishserver so that test scripts do not need to include the AUT's path, thus making the tests platform-independent. Another benefit of registering is that AUTs can be tested without the Squish IDE—for example, when doing regression testing.

    This is done by executing the squishserver on the command line with the --config option and the addAUT command. For example, assuming we are in the SQUISHDIR/bin directory, on Linux:

    ./squishserver --config addAUT addressbook.tcl $SQUISHDIR/examples/tk/addressbook

    Windows users can add a .bat file that starts the AUT, or tclsh.exe as the AUT like this:

    squishserver --config addAUT tclsh.exe C:\TCL\bin

    We must give the addAUT command the name of the AUT's executable and—separately—the AUT's path. (For more information about application paths, see AUTs and Settings in the User Guide, and for more about the squishserver's command line options see squishserver in the Tools Reference.)

Recording Tests and Verification Points

Squish records tests using the scripting language that was specified for the test suite. Once a test has been recorded, we can run the test and Squish will faithfully repeat all the actions that we performed when recording the test, but without the pauses that humans are prone to but which computers don't need. It is also possible and very common to edit recorded tests, or to copy parts of recorded tests into manually created tests, as we will see later on in the tutorial.

Recordings are made into existing test cases. You can create a New Script Test Case in the following ways:

  • Select File > New Test Case to open the New Squish Test Case wizard, enter the name for the test case, and select Finish.
  • Click the New Script Test Case ( ) toolbar button to the right of the Test Cases label in the Test Suites view. This creates a new test case with a default name, which you can easily change.

Give the new test case the name "tst_general".

Squish automatically creates a sub-folder inside the test suite's folder with this name and also a test file, for example test.py. If you choose JavaScript as the scripting language, the file is called test.js, and correspondingly for Perl, Ruby, or Tcl.

"The tst_general test case"

If you get a sample .feature file instead of a "Hello World" script, click the arrow left of the Run Test Suite ( ) and select New Script Test Case ( ).

To make the test script file (such as, test.js or test.py) appear in an Editor view, click or double-click the test case, depending on the Preferences > General > Open mode setting. This selects the Script as the active one and makes visible its corresponding Record ( ) and Run Test ( ) buttons.

The checkboxes are used to control which test cases are run when the Run Test Suite ( ) toolbar button is clicked. We can also run a single test case by clicking its Run Test ( ) button. If the test case is not currently active, the button may be invisible until the mouse is hovered over it.

Initially, the script is empty. If we were to create a test manually (as we will do later on in the tutorial), we must create a main function. The name "main" is special to Squish—tests may contain as many functions and other code as we like (providing it is legal for the scripting language), but when the test is executed (i.e., run), Squish always executes the main function. This is actually very convenient since it means we are free to create other functions, import libraries, and so on, without problems. It is also possible to share commonly used code between test scripts—this is covered in the User Guide. Two additional function names are special to Squish, cleanup and init. See Tester-Created Special Functions for details.

Once the new empty test case has been created, we are now free to write test code manually, or to record a test. If we choose to record, we can either replace all the test's code with the recorded code, or record a snippet into the middle of some existing test code.

Creating Tests from Command Line

To create a new test case from the command line:

  1. Create a new subdirectory inside the test suite directory. For example, inside the SQUISHDIR/examples/tk/addressbook/suite_tcl directory, create the tst_general directory.
  2. Inside the test case's directory create an empty file called test.tcl (or test.js if you are using the JavaScript scripting language, and similarly for the other languages).

Recording Our First Test

Before we dive into recording let's briefly review our very simple test scenario:

  1. Open the MyAddresses.adr address file.
  2. Navigate to the second address and then add a new name and address.
  3. Navigate to the fourth address (that was the third address) and change the surname field.
  4. Navigate to the first address and remove it.
  5. Verify that the first address is now the new one that was added.

We are now ready to record our first test. Click the Record ({} ) to the right of the tst_general test case shown in the Test Suites view's Test Cases list. This will cause Squish to run the AUT so that we can interact with it. Once the AUT is running, perform the following actions—and don't worry about how long it takes since Squish doesn't record idle time:

  1. From the main menu bar, click File > Open, and once the file dialog appears, click somewhere in the line input widget for Filename, and type into it: MyAddresses.adr, then click the Open button.
  2. Click the second row, then from the menu bar, click Edit > Add, then in the Add dialog's first line edit type in "Jane". Now click (or tab to) the second line edit and type in "Doe". Continue similarly, to set an email address of "jane.doe@nowhere.com" and a phone number of "555 123 4567". Don't worry about typing mistakes—just backspace delete as normal and fix them. Finally, click the OK button. There should now be a new second address with the details you typed in.
  3. Click the fourth row, then click Edit > Edit to pop up the Edit dialog. In this dialog change the surname to "Doe" and click Ok to confirm the change.
  4. Now click the first row, then click Edit > Remove. The first row should be gone, so your "Jane Doe" entry should now be the first one.
  5. Click the first row, then click Edit > Edit to pop up the Edit dialog (which should show the "Jane Doe" entry's details).
  6. Now click the Verify toolbar button in the Squish Control Bar Window (the second button from the left) and select Properties.

    {}

    This will make the Squish IDE appear.

  7. In the Application Objects view expand the addressbook.tcl Tk_Window object, and then the dialog object. Click the "forename" object to make its properties appear in the Properties view, and then check the getvalue property's checkbox. Now click the "surname" object and check its getvalue property.
  8. Finally, click the Save and Insert Verifications button (at the bottom of the Verification Point Creator view) to have the forename and surname verifications for the first row inserted into the recorded test script. (See the screenshot below.) Once the verification points are inserted the Squish IDE's window will be hidden again and the Control Bar window and the AUT will be back in view.
  9. We've now completed the test plan, so in the AUT, click Cancel to close the Edit dialog, then click File > Quit, then click No in the message box, since we don't want to save any changes.

"Two verification points about to be inserted"

Once we quit the AUT, the recorded test will appear in Squish's IDE as the screenshot illustrates. (Note that the exact code that is recorded will vary depending on how you interact. For example, you might invoke menu options by clicking them or by using key sequences—it doesn't matter which you use, but since they are different, Squish will record them differently.)

"The recorded tst_general test"

If the recorded test doesn't appear, click (or double-click depending on your platform and settings) the tst_general test case; this will make Squish show the test's test.tcl file in an editor window as shown in the screenshot.

Now that we've recorded the test, we can play it back, i.e., run it. Now, the two verifications we put in will be checked on play back as the screenshot shows.

Inserting verification points during test recording is very convenient. Here we inserted two in one go, but we can insert as many as we like as often as we like during the test recording process. However, sometimes we might forget to insert one, or later we might want to insert a new one. We can insert additional verifications into a recorded test script, as we will see in the next section, Inserting Additional Verification Points.

Before going further, we will look at how to record a test from the command line. Then we will see how to run a test, and we will also look at some of the code that Squish generated to record the test and discuss some of its features.

Recording Tests from Command Line

The squishserver must always be running when recording a test. This is handled automatically by the Squish IDE, but command line users must start the squishserver manually, as instructed in squishserver.

To record a test from the command line, execute the squishrunner program and specify the test suite to record inside and the name of the test case. For example, assuming we are in the directory that contains the test suite's directory:

squishrunner --testsuite suite_tcl --record tst_general --useWaitFor

It is always best to record using the --useWaitFor option that records calls to the Object waitForObject(objectOrName) function, which is more reliable than using the snooze(seconds) function, even though it is the default for historical reasons. The Squish IDE automatically uses the Object waitForObject(objectOrName) function.

When you have multiple devices and/or emulators attached, then you need to specify the target using --device some-device

Running Tests from IDE

To run a test case in the IDE, click the Run Test ( ) that appears when the test case is hovered or selected in the Test Suites view.

To run two or more test cases one after another or to run only the selected test cases, clic Run Test Suite ( ).

Running Tests from Command Line

The squishserver must always be running when running a test, or the --local option must be provided to squishrunner. For more information, see squishserver.

To play back a recorded test from the command line, we execute the squishrunner program and specify the test suite our recorded script is in and the test case we want to play. For example, assuming we are in the directory that contains the test suite's directory:

squishrunner --testsuite suite_tcl --testcase tst_general --local

Examining the Generated Code

If you look at the code in the screenshot (or the code snippet shown below), you will see that it consists of lots of Object waitForObject(objectOrName) calls as parameters to various other calls such as activateItem(objectName, itemText), clickItem(objectName, itemIdentifier, x, y, modifierState, button), clickButton(objectName), and type(objectName, text), as well as many calls to the Object waitForObjectItem(objectOrName, itemOrIndex). The Object waitForObject(objectOrName) and Object waitForObjectItem(objectOrName, itemOrIndex) functions wait until a GUI object is ready to be interacted with (i.e., becomes visible and enabled), and is then followed by some function that interacts with the object. The typical interactions are activate (pop-up) a menu, click a menu option or a button, or type in some text. (For a complete overview of Squish's script commands see the User Guide, the API Reference, and the Tools Reference. Objects are identified by names that Squish generates. See How to Identify and Access Objects.

The generated code is about 80 lines of code. Here's an extract that just shows how Squish records clicking the Edit menu's Add option, typing in Jane Doe's details into the Add dialog, and clicking OK at the end to close the dialog and update the table.

Note: Although the screenshots only show the Python test suite in action, for the code snippets quoted here and throughout the tutorial, we show the code for all the scripting languages that Squish supports. In practice you would normally only use one of them of course, so feel free to just look at the snippets in the language you are interested in and skip the others.

    waitForObjectItem ":addressbook\\.tcl.#menuBar" "Edit"
    invoke activateItem ":addressbook\\.tcl.#menuBar" "Edit"
    waitForObjectItem ":addressbook\\.tcl.#menuBar.#edit" "Add..."
    invoke activateItem ":addressbook\\.tcl.#menuBar.#edit" "Add..."
    invoke type [waitForObject ":addressbook\\.tcl.dialog.forename"] "<Shift_L>"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.forename"] "J" 17
    invoke type [waitForObject ":addressbook\\.tcl.dialog.forename"] "ane"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.forename"] "<Tab>"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.surname"] "<Shift_L>"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.surname"] "D" 17
    invoke type [waitForObject ":addressbook\\.tcl.dialog.surname"] "oe"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.surname"] "<Tab>"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.phone"] "555"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.phone"] "<space>"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.phone"] "123"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.phone"] "<space>"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.phone"] "4567"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.phone"] "<Tab>"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.email"] "jane"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.email"] "<period>"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.email"] "doe"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.email"] "<Shift_L>"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.email"] "<at>" 17
    invoke type [waitForObject ":addressbook\\.tcl.dialog.email"] "nowhere"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.email"] "<period>"
    invoke type [waitForObject ":addressbook\\.tcl.dialog.email"] "com"
    invoke clickButton [waitForObject ":addressbook\\.tcl.dialog.buttonarea.ok"]
    waitForObjectItem(":addressbook\\.tcl.#menuBar", "Edit")
    activateItem(":addressbook\\.tcl.#menuBar", "Edit")
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#edit", "Add...")
    activateItem(":addressbook\\.tcl.#menuBar.#edit", "Add...")
    type(waitForObject(":addressbook\\.tcl.dialog.forename"), "<Shift_L>")
    type(waitForObject(":addressbook\\.tcl.dialog.forename"), "J", 17)
    type(waitForObject(":addressbook\\.tcl.dialog.forename"), "ane")
    type(waitForObject(":addressbook\\.tcl.dialog.forename"), "<Tab>")
    type(waitForObject(":addressbook\\.tcl.dialog.surname"), "<Shift_L>")
    type(waitForObject(":addressbook\\.tcl.dialog.surname"), "D", 17)
    type(waitForObject(":addressbook\\.tcl.dialog.surname"), "oe")
    type(waitForObject(":addressbook\\.tcl.dialog.surname"), "<Tab>")
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "555")
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "<space>")
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "123")
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "<space>")
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "4567")
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "<Tab>")
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "jane")
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "<period>")
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "doe")
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "<Shift_L>")
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "<at>", 17)
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "nowhere")
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "<period>")
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "com")
    clickButton(waitForObject(":addressbook\\.tcl.dialog.buttonarea.ok"))
    waitForObjectItem(":addressbook\\.tcl.#menuBar", "Edit");
    activateItem(":addressbook\\.tcl.#menuBar", "Edit");
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#edit", "Add...");
    activateItem(":addressbook\\.tcl.#menuBar.#edit", "Add...");
    type(waitForObject(":addressbook\\.tcl.dialog.forename"), "<Shift_L>");
    type(waitForObject(":addressbook\\.tcl.dialog.forename"), "J", 17);
    type(waitForObject(":addressbook\\.tcl.dialog.forename"), "ane");
    type(waitForObject(":addressbook\\.tcl.dialog.forename"), "<Tab>");
    type(waitForObject(":addressbook\\.tcl.dialog.surname"), "<Shift_L>");
    type(waitForObject(":addressbook\\.tcl.dialog.surname"), "D", 17);
    type(waitForObject(":addressbook\\.tcl.dialog.surname"), "oe");
    type(waitForObject(":addressbook\\.tcl.dialog.surname"), "<Tab>");
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "555");
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "<space>");
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "123");
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "<space>");
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "4567");
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "<Tab>");
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "jane");
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "<period>");
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "doe");
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "<Shift_L>");
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "<at>", 17);
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "nowhere");
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "<period>");
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "com");
    clickButton(waitForObject(":addressbook\\.tcl.dialog.buttonarea.ok"));
    waitForObjectItem(":addressbook\\.tcl.#menuBar", "Edit");
    activateItem(":addressbook\\.tcl.#menuBar", "Edit");
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#edit", "Add...");
    activateItem(":addressbook\\.tcl.#menuBar.#edit", "Add...");
    type(waitForObject(":addressbook\\.tcl.dialog.forename"), "<Shift_L>");
    type(waitForObject(":addressbook\\.tcl.dialog.forename"), "J", 17);
    type(waitForObject(":addressbook\\.tcl.dialog.forename"), "ane");
    type(waitForObject(":addressbook\\.tcl.dialog.forename"), "<Tab>");
    type(waitForObject(":addressbook\\.tcl.dialog.surname"), "<Shift_L>");
    type(waitForObject(":addressbook\\.tcl.dialog.surname"), "D", 17);
    type(waitForObject(":addressbook\\.tcl.dialog.surname"), "oe");
    type(waitForObject(":addressbook\\.tcl.dialog.surname"), "<Tab>");
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "555");
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "<space>");
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "123");
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "<space>");
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "4567");
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "<Tab>");
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "jane");
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "<period>");
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "doe");
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "<Shift_L>");
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "<at>", 17);
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "nowhere");
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "<period>");
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "com");
    clickButton(waitForObject(":addressbook\\.tcl.dialog.buttonarea.ok"));
    waitForObjectItem(":addressbook\\.tcl.#menuBar", "Edit")
    activateItem(":addressbook\\.tcl.#menuBar", "Edit")
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#edit", "Add...")
    activateItem(":addressbook\\.tcl.#menuBar.#edit", "Add...")
    type(waitForObject(":addressbook\\.tcl.dialog.forename"), "<Shift_L>", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.forename"), "J", 1)
    type(waitForObject(":addressbook\\.tcl.dialog.forename"), "ane", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.forename"), "<Tab>", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.surname"), "<Shift_L>", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.surname"), "D", 1)
    type(waitForObject(":addressbook\\.tcl.dialog.surname"), "oe", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.surname"), "<Tab>", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "555", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "<space>", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "123", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "<space>", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "4567", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.phone"), "<Tab>", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "jane", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "<period>", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "doe", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "<Shift_L>", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "<Shift_L>", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "<at>", 1)
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "nowhere", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "<period>", 0)
    type(waitForObject(":addressbook\\.tcl.dialog.email"), "com", 0)
    clickButton(waitForObject(":addressbook\\.tcl.dialog.buttonarea.ok"))

While recording the test above, the tester used the keyboard to tab from one text field to another and clicked OK using the mouse, rather than with a key press. If the tester had clicked the button any other way (for example, by tabbing to the OK button and pressing the spacebar), the outcome should be the same, but Squish will have recorded different actions.

Object Names

Some object names are strings that begin with a colon. These are symbolic names. Squish supports several naming schemes, all of which can be used—and mixed—in scripts.

The advantage of using symbolic names is that if the application changes in a way that results in different names being generated, we can simply update Squish's Object Map (which relates symbolic names to real names), and thereby avoid the need to change our test scripts. See the Object Map and the Object Map view for more about the Object Map.

Unlike other editions of Squish, the real names used in Squish/Tk are hierarchical instead of multi-property names. This means that the object name represents a path of class names in the object tree that stores the GUI in memory. Symbolic names are based on this hierarchy also.

Now that we have seen how to record and play back a test and have seen the code that Squish generates, let's go a step further and make sure that at particular points in the test's execution certain conditions hold.

Inserting Additional Verification Points

In the previous section, we saw how easy it is to insert verification points during the recording of test scripts. Verification points can also be inserted into existing test scripts, either by setting a breakpoint and using the Squish IDE, or simply by editing a test script and putting in calls to Squish's test functions such as Boolean test.compare(value1, value2) and Boolean test.verify(condition).

Squish supports four kinds of verification points: those that verify that object properties have particular values—known as "Object Property Verifications"; those that verify that an entire table has the contents we expect—known as "Table Verifications"; those that verify that two images match—known as "Screenshot Verifications"; and a hybrid verification type that includes properties and screenshots from multiple objects, known as "Visual Verifications". The most commonly used kind is object property verifications, and it is these that we will cover in the tutorial. For further reading, see How to Create and Use Verification Points).

Regular (non-scriptified) property verification points are stored as XML files in the test case or test suite resources, and contain the value(s) that need to be passed to test.compare(). These verification points can be reused across test cases, and can verify many values in a single line of script code.

Scriptified property verification points are direct calls to the Boolean test.compare(value1, value2) function, with two arguments—the value of a particular property for a particular object, and an expected value. We can manually insert calls to the Boolean test.compare(value1, value2) function in a recorded or hand written script, or we can get Squish to insert them for us using scriptified verification points. In the previous section we showed how to use the Squish IDE to insert verifications during recording. Here we will first show how to use the Squish IDE to insert verifications into an existing test script, and then we will show how to insert a verification by hand.

Before asking Squish to insert verification points, it is best to make sure that we have a list of what we want to verify and when. There are many potential verifications we could add to the tst_general test case, but since our concern here is simply to show how to do it, we will only do two—we will verify that the "Jane Doe" entry's email address and phone number match the ones entered, and put the verifications immediately after the ones we inserted during recording.

To insert a verification point using the IDE, we start by putting a break point in the script (whether recorded or manually written—it does not matter to Squish), at the point where we want to verify.

"The tst_general test case with a breakpoint"

As the above screenshot shows, we have set a breakpoint at line 71. This is done simply by right-clicking the line number and then clicking the Toggle Breakpoint menu item in the context menu. We chose this line because it follows the script lines where the first address is removed, and where we have invoked the Edit dialog. So at this point (just before invoking the File menu to close the application), the first address (and the one shown in the Edit dialog) should be that of "Jane Doe". The screenshot shows the verifications that were entered using the Squish IDE during recording. Our additional verifications will follow them. (Note that your line number may be different if you recorded the test in a different way, for example, using keyboard shortcuts rather than clicking menu items.)

Having set the breakpoint, we now run the test as usual by clicking the Run Test ({} ) or by clicking the Run > Run Test Case menu option. Unlike a normal test run the test will stop when the breakpoint is reached (i.e., at line 71, or at whatever line you set), and Squish's main window will reappear (which will probably obscure the AUT). At this point the Squish IDE will automatically switch to the Squish Test Debugging Perspective.

Perspectives and Views

The Squish IDE works just like the Eclipse IDE. If you aren't used to Eclipse, it is crucial to understand the following key concepts: Views and Perspectives. In Eclipse, and therefore in the Squish IDE, a View is essentially a child window, such as a dock window or a tab in an existing window. A Perspective is a collection of views arranged together. Both are accessible through the Window menu.

The Squish IDE is supplied with the following perspectives:

You can change these perspectives to show additional views or to hide views that you don't want, or create your own perspectives with exactly the views you want. So if your windows change dramatically, it just means that the perspective changed. Use the Window menu to change back to the perspective you want. However, Squish automatically changes perspective to reflect the current situation, so you should not need to change perspective manually.

Inserting Verification Points

As the screenshot below shows, when Squish stops at a breakpoint the Squish IDE automatically changes to the Squish Test Debugging Perspective. The perspective shows the Variables view, the Editor view, the Debug view, the Application Objects view, and the Properties view, Methods view, and Test Results view.

To insert a verification point we can expand items in the Application Objects view until we find the object we want to verify. In this example we want to verify the table's first row's texts, so we expand the addressbook.tcl Tk_Window object, and then the dialog object to find the items we are interested in: in this case the phone and email items. Once we click an object its properties are shown in the Properties view as the screenshot shows.

"Picking an object to verify in the Application Objects view"

The normal Squish Test Management Perspective can be returned to at any time by choosing it from the Window menu (or by clicking its toolbar button), although the Squish IDE will automatically return to it if you stop the script or run it to completion.

Here, we can see that the getvalue property of the forename item has the value "Jane"; we already have a verification for this that we inserted during recording. Scroll down so that you can see the phone item. To make sure that this is verified every time the test is run, click the phone item in the Application Objects view to make its properties appear, and then click the getvalue property to check its check box. When we check it the Verification Point Creator view appears as shown in the screenshot.

"Choosing a property value to verify"

At this point the verification point has not been added to the test script. We could easily add it by clicking the Save and Insert Verifications button. But before doing that we'll add one more thing to be verified.

Scroll down and click the email item in the Application Objects view; then click its getvalue property. Now both verifications will appear in the Verification Point Creator view as the screenshot shows.

"Choosing several property values to verify"

We have now said that we expect these properties to have the values shown, that is, an email address of "jane.doe@nowhere.com" and phone number of "555 123 4567". We must click the Insert button to actually insert the verification point, so do that now.

We don't need to continue running the test now, so we can either stop running the test at this point (by clicking the Stop toolbar button), or we can continue (by clicking the Resume button).

Once we have finished inserting verifications and stopped or finished running the test we should now disable the break point. Just right click the break point and click the Disable Breakpoint menu option in the context menu. We are now ready to run the test without any breakpoints but with the verification points in place. Click the Run Test ({} ) button. This time we will get some additional test results—as the screenshot shows—one of which we have expanded to show its details. (We have also selected the lines of code that Squish inserted to perform the verifications—notice that the code is structurally identical to the code inserted during recording.)

"Newly inserted verification points"

These particular verification points generate four tests comparing the forename, surname, phone number, and email of the newly inserted entry.

Another way to insert verification points is to insert them in code form. In theory we can just add our own calls to Squish's test functions such as Boolean test.compare(value1, value2) and Boolean test.verify(condition) anywhere we like in an existing script. In practice it is best to make sure that Squish knows about the objects we want to verify first so that it can find them when the test is run. This involves a very similar procedure as using the Squish IDE. First we set a breakpoint where we intend adding our verifications. Then we run the test script until it stops. Next we navigate in the Application Objects view until we find the object we want to verify. At this point it is wise to right-click the object we are interested in and click the Add to Object Map context menu option. This will ensure that Squish can access the object. Then right click again and click the Copy to clipboard (Symbolic Name) context menu option—this gives us the name of the object that Squish will use to identify it. Now we can edit the test script to add in our own verification and finish or stop the execution. (Don't forget to disable the break point once it isn't needed any more.)

Although we can write our test script code to be exactly the same style as the automatically generated code, it is usually clearer and easier to do things in a slightly different style, as we will explain in a moment.

For our manual verifications we want to check the number of addresses present in the table after reading in the MyAddresses.adr file, then after the new address is added, and finally after the first address is removed. The screenshot shows three of the lines of code we entered to get one of these three verifications, plus the results of running the test script.

"Manually entered verification points"

When writing scripts by hand, we use Squish's test module's functions to verify conditions at certain points during our test script's execution. As the screenshot (and the code snippets below) show, we begin by making sure that the object we are interested in is available. Using the Object waitForObject(objectOrName) function is standard practice for manually written test scripts. This function waits for the object to be available (i.e., visible and enabled). (Otherwise it times out and raises a catchable exception.) Once we know that the object is available we use the String tcleval(code) function to interact with the object—in this case to retrieve the number of rows (which the table reports as its "size"). We then compare the actual number of rows with the expected number using the Boolean test.compare(value1, value2) function. Notice that Squish functions normally require object names prefixed by the AUT's name, but the String tcleval(code) function does not need the prefix.

Here is the code we entered manually for the first verification for all the scripting languages that Squish supports. Naturally, you only need to look at the code for the language that you will be using for your own tests. It is usually more convenient in Tcl to use the Boolean test.compare(value1, value2) function, but we can of course use any of Squish's test functions, such as the Boolean test.verify(condition) function which we use for the other scripting languages.

    waitForObject ":addressbook\\.tcl.view.tree"
    set rows [invoke tcleval ".view.tree size"]
    test compare $rows "125"
    waitForObject(":addressbook\\.tcl.view.tree")
    rows = tcleval(".view.tree size")
    test.verify(rows == "125")
    waitForObject(":addressbook\\.tcl.view.tree");
    var rows = tcleval(".view.tree size");
    test.verify(parseInt(rows) == 125);
    waitForObject(":addressbook\\.tcl.view.tree");
    my $rows = tcleval(".view.tree size");
    test::verify($rows eq 125);
    waitForObject(":addressbook\\.tcl.view.tree")
    rows = tcleval(".view.tree size")
    Test.verify(rows == "125")

The coding pattern is very simple: we ensure that the object we are interested in is available, access its properties using the String tcleval(code) function, and then verify the results using Squish's verification functions.

We will see more examples of manually written code shortly, in the Creating Tests by Hand section, and further examples are in the User Guide.

For complete coverage of verification points, see How to Create and Use Verification Points in the User Guide.

Test Results

After each test run finishes, the test results—including those for the verification points—are shown in the Test Results view at the bottom of the Squish IDE.

This is a detailed report of the test run and would also contain details of any failures or errors, etc. If you click on a Test Results item, the Squish IDE highlights the script line which generated the test result. And if you expand a Test Results item, you can see additional details of the test.

Squish's interface for test results is very flexible. By implementing custom report generators it is possible to process test results in many different ways, for example to store them in a database, or to output them as HTML files. The default report generator simply prints the results to stdout when Squish is run from the command line, or to XML and the Test Results view when Squish's IDE is being used. You can Export the XML test results from the Squish IDE by right clicking on the Test Results and choosing the Export button or menu action.

If you run tests on the command line using squishrunner, you can also export the results in different formats and save them to files. For a list of report generators available, see squishrunner --reportgen: Generating Reports. See the sections Processing Test Results and How to Use Test Statements in the User Guide for more information. It is also possible to log test results directly to a database. See How to Access Databases from Squish Test Scripts.

Creating Tests by Hand

Now that we have seen how to record a test and modify it by inserting verification points, we are ready to see how to create tests manually. The easiest way to do this is to modify and refactor recorded tests, although it is also perfectly possible to create manual tests from scratch.

Potentially the most challenging part of writing manual tests is to use the right object names, but in practice, this is rarely a problem. We can either copy the symbolic names that Squish has already added to the Object Map when recording previous tests, or we can copy object names directly from recorded tests. And if we haven't recorded any tests and are starting from scratch we can use the Spy. We do this by clicking the Launch AUT toolbar button. This starts the AUT and switches to the Squish Spy Perspective. We can then interact with the AUT until the object we are interested in is visible. Then, inside the Squish IDE we can navigate to the object in the Application Objects view and use the context menu to both Add to Object Map (so that Squish will remember it) and to Copy Real Name to clipboard (so that we can paste it into our test script). When finished, we can click the Quit AUT toolbar button to terminate the AUT and return Squish to the Squish Test Management Perspective.

To see the object map entry for a particular symbolic name, you can highlight it in the Editor view, right click, and select Open Symbolic Name. We can also see the Object Map by clicking the Object Map ({} ) button (see also, the Object Map view). Every application object that Squish interacts with is listed here, either as a top-level object, or as a child object (the view is a tree view). We can retrieve the object names used for it by right-clicking the object map entry and clicking Copy Object Name or, if a string is desired instead, Copy Real Name. This is useful for when we want to modify existing test scripts or when we want to create test scripts from scratch, as we will see later on in the tutorial.

"Squish Object Map"

Modifying and Refactoring Recorded Tests

Suppose we want to test the AUT's Add functionality by adding three new names and addresses. We could record such a test, but it may be easier to code things manually. The steps we need the test script to do are: first click File > New to create a new address book, then for each new name and address:

  • Click Edit > Add.
  • Fill in the details.
  • Click OK.

Finally, click File > Quit without saving.

We also want to verify at the start that there are no rows of data and at the end that there are three rows. We will refactor as we go, to make our code as neat and modular as possible.

First we must create a new empty test case. Click File > New Test Case and set the test case's name to be tst_adding. Squish will automatically create an empty test.tcl (or test.js, and so on) file.

Command line users can simply create a tst_adding directory inside the test suite's directory and create and edit the test.tcl file (or test.js and so on) within that directory.

The first thing we need is a way to start the AUT and then invoke a menu option. Here are the first few lines from the recorded tst_general script:

proc main {} {
    startApplication "addressbook.tcl"
    waitForObjectItem ":addressbook\\.tcl.#menuBar" "File"
    invoke activateItem ":addressbook\\.tcl.#menuBar" "File"
    waitForObjectItem ":addressbook\\.tcl.#menuBar.#file" "Open..."
    invoke activateItem ":addressbook\\.tcl.#menuBar.#file" "Open..."
def main():
    startApplication("addressbook.tcl")
    waitForObjectItem(":addressbook\\.tcl.#menuBar", "File")
    activateItem(":addressbook\\.tcl.#menuBar", "File")
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#file", "Open...")
    activateItem(":addressbook\\.tcl.#menuBar.#file", "Open...")
function main()
{
    startApplication("addressbook.tcl");
    waitForObjectItem(":addressbook\\.tcl.#menuBar", "File");
    activateItem(":addressbook\\.tcl.#menuBar", "File");
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#file", "Open...");
    activateItem(":addressbook\\.tcl.#menuBar.#file", "Open...");
sub main
{
    startApplication("addressbook.tcl");
    waitForObjectItem(":addressbook\\.tcl.#menuBar", "File");
    activateItem(":addressbook\\.tcl.#menuBar", "File");
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#file", "Open...");
    activateItem(":addressbook\\.tcl.#menuBar.#file", "Open...");
def main
    startApplication("addressbook.tcl")
    waitForObject(":addressbook\\.tcl.#menuBar")
    activateItem(":addressbook\\.tcl.#menuBar", "")
    waitForObjectItem(":addressbook\\.tcl.#menuBar", "File")
    activateItem(":addressbook\\.tcl.#menuBar", "File")
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#file", "Open...")
    activateItem(":addressbook\\.tcl.#menuBar.#file", "Open...")

Notice that the pattern in the code is simple: start the AUT, then wait for the menu bar, then activate the menu bar; wait for the menu item, then activate the menu item. In both cases we have used the Object waitForObjectItem(objectOrName, itemOrIndex) function. This function is used for a multi-valued objects (such as lists, tables, trees—or in this case, a menubar and a menu), and allows us to access the object's items (which are themselves objects of course), by passing the name of the object containing the item and the item's text as arguments.

Note: It may seem a waste to put our functions in tst_adding because we could also use them in tst_general and in other test cases. However, to keep the tutorial simple we will put the code in the tst_adding test case. See How to Create and Use Shared Data and Shared Scripts for how to share scripts.

If the AUT appears to freeze during test execution, wait for Squish to time out the AUT (about 20 seconds), and show the Object Not Found dialog, indicating an error like this:

Error Script Error Apr 9, 2010
Detail LookupError: Item 'New...' in object ':addressbook.tcl' not found or ready.
Called from: C:\squish\examples\tk\addressbook\suite_py\tst_adding\test.tcl: 18
Location C:\squish\examples\tk\addressbook\suite_py\tst_adding\test.tcl:3

This usually means that Squish doesn't have an object with the given name, or property values, in the Object Map. From here, we can Pick a new object, Debug, Throw Error or, after picking a new object, Retry. In addition to the Spy's Object Picker ({} ) we can use the Application Objects view to locate the objects we are interested in and use a context menu action to Add to the Object Map. In general, recording a dummy test case that interacts with all of the relevant AUT objects is a much more efficient way to initially populate the Object Map.

Naming is important because it is probably the part of writing scripts that leads to the most error messages, usually of the object ... not found kind shown above. Once we have identified the objects to access in our tests, writing test scripts using Squish is very straightforward. Especially, as Squish most likely supports the scripting language you are most familiar with.

We are now almost ready to write our own test script. It is probably easiest to begin by recording a dummy test. So click File > New Test Case and set the test case's name to be tst_dummy. Then click the dummy test case's Record ({} ). Once the AUT starts, click File > New, then click the (empty) table, then click Edit > Add and add an item, then press Return or click OK. Finally, click File > Quit to finish, and say No to saving changes.

Replay this test just to confirm that everything works okay. The sole purpose of this is to make sure that Squish adds the necessary names to the Object Map since it is probably quicker to do it this way than to use the Spy for every object of interest. After replaying the dummy test you can delete it if you want to.

proc main {} {
    startApplication "addressbook.tcl"
    invokeMenuItem "File" "New"
    verifyRows 0
    set data [list \
        [list "Andy" "Beach" "andy.beach@nowhere.com" "555 123 6786"] \
        [list "Candy" "Deane" "candy.deane@nowhere.com" "555 234 8765"] \
        [list "Ed" "Fernleaf" "ed.fernleaf@nowhere.com" "555 876 4654"] ]
    for {set i 0} {$i < [llength $data]} {incr i} {
        addNameAndAddress [lindex $data $i]
    }
    verifyRows [llength $data]
    closeWithoutSaving
}
def main():
    startApplication("addressbook.tcl")
    invokeMenuItem("File", "New")
    verifyRows(0)
    data = [("Andy", "Beach", "andy.beach@nowhere.com", "555 123 6786"),
            ("Candy", "Deane", "candy.deane@nowhere.com", "555 234 8765"),
            ("Ed", "Fernleaf", "ed.fernleaf@nowhere.com", "555 876 4654")]
    for oneNameAndAddress in data:
        addNameAndAddress(oneNameAndAddress)
    verifyRows(len(data))
    closeWithoutSaving()
function main()
{
    startApplication("addressbook.tcl");
    invokeMenuItem("File", "New");
    verifyRows(0);
    var data = new Array(
        new Array("Andy", "Beach", "andy.beach@nowhere.com", "555 123 6786"),
        new Array("Candy", "Deane", "candy.deane@nowhere.com", "555 234 8765"),
        new Array("Ed", "Fernleaf", "ed.fernleaf@nowhere.com", "555 876 4654"));
    for (var row = 0; row < data.length; ++row)
        addNameAndAddress(data[row]);
    verifyRows(data.length);
    closeWithoutSaving();
}
sub main
{
    startApplication("addressbook.tcl");
    invokeMenuItem("File", "New");
    verifyRows(0);
    my @data = (["Andy", "Beach", "andy.beach\@nowhere.com", "555 123 6786"],
                ["Candy", "Deane", "candy.deane\@nowhere.com", "555 234 8765"],
                ["Ed", "Fernleaf", "ed.fernleaf\@nowhere.com", "555 876 4654"]);
    foreach $oneNameAndAddress (@data) {
        addNameAndAddress(@{$oneNameAndAddress});
    }
    verifyRows(scalar(@data));
    closeWithoutSaving;
}
def main
    startApplication("addressbook.tcl")
    invokeMenuItem("File", "New")
    verifyRows(0)
    data = [["Andy", "Beach", "andy.beach@nowhere.com", "555 123 6786"],
        ["Candy", "Deane", "candy.deane@nowhere.com", "555 234 8765"],
        ["Ed", "Fernleaf", "ed.fernleaf@nowhere.com", "555 876 4654"]]
    data.each do |oneNameAndAddress|
        addNameAndAddress(oneNameAndAddress)
    end
    verifyRows(data.length)
    closeWithoutSaving
end

We begin by starting the application with a call to the ApplicationContext startApplication(autName) function. The name we pass as a string is the name registered with Squish (normally the name of the executable).

The invokeMenuItem function is one we have created specially for this test. It takes a menu name and a menu option name and invokes the menu option. After using the invokeMenuItem function to do File > New, we verify that the table's row count is 0. The Boolean test.verify(condition) function is useful when we simply want to verify that a condition is true rather than compare two different values. (For Tcl we usually use the Boolean test.compare(value1, value2) function rather than the Boolean test.verify(condition) function simply because it is slightly simpler to use in Tcl.)

Next, we create some sample data and call a custom addNameAndAddress to populate the table with the data using the AUT's Add dialog. Then we again compare the table's row count, this time to the number of rows in our sample data. And finally we call a custom closeWithoutSaving to terminate the application.

We will now review each of the four supporting functions (and a fifth helper function), so as to cover all the code in the tst_adding test case, starting with invokeMenuItem.

proc invokeMenuItem {menu item} {
    waitForObjectItem ":addressbook\\.tcl.#menuBar" $menu
    invoke activateItem ":addressbook\\.tcl.#menuBar" $menu
    set menuName [string tolower $menu]
    waitForObjectItem ":addressbook\\.tcl.#menuBar.#$menuName" $item
    invoke activateItem ":addressbook\\.tcl.#menuBar.#$menuName" $item
}
def invokeMenuItem(menu, item):
    waitForObjectItem(":addressbook\\.tcl.#menuBar", menu)
    activateItem(":addressbook\\.tcl.#menuBar", menu)
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#%s" % menu.lower(), item)
    activateItem(":addressbook\\.tcl.#menuBar.#%s" % menu.lower(), item)
function invokeMenuItem(menu, item)
{
    waitForObjectItem(":addressbook\\.tcl.#menuBar", menu);
    activateItem(":addressbook\\.tcl.#menuBar", menu);
    var menuText = menu.toLowerCase();
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#" + menuText, item);
    activateItem(":addressbook\\.tcl.#menuBar.#" + menuText, item);
}
sub invokeMenuItem
{
    my ($menu, $item) = @_;
    waitForObjectItem(":addressbook\\.tcl.#menuBar", $menu);
    activateItem(":addressbook\\.tcl.#menuBar", $menu);
    my $menuText = lc $menu;
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#$menuText", $item);
    activateItem(":addressbook\\.tcl.#menuBar.#$menuText", $item);
}
def invokeMenuItem(menu, item)
    waitForObjectItem(":addressbook\\.tcl.#menuBar", menu)
    activateItem(":addressbook\\.tcl.#menuBar", menu)
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#%s" % menu.downcase, item)
    activateItem(":addressbook\\.tcl.#menuBar.#%s" % menu.downcase, item)
end

Symbolic names are based on the hierarchy of an object in its GUI. In Squish/Tk, real names are strings that also represent that hierarchy. Here, we've used the string names that Squish generated, except that for the menus and item, we parametrize the label text.

Once we have identified the object we want to interact with, we use the Object waitForObjectItem(objectOrName, itemOrIndex) function to retrieve a reference to it and in this case we then apply the activateItem(objectName, itemText) function to it. The Object waitForObjectItem(objectOrName, itemOrIndex) function pauses Squish until the specified object and its item are visible and enabled. So, here, we waited for the menu bar and one of its menu bar items, and then we waited for a menu bar item and one of its menu items. And as soon as the waiting is over each time we activate the object and its item using the activateItem(objectName, itemText) function.

proc verifyRows {expected_rows} {
    waitForObject ":addressbook\\.tcl.view.tree"
    set rows [invoke tcleval ".view.tree size"]
    test compare $rows "$expected_rows"
}
def verifyRows(expected_rows):
    waitForObject(":addressbook\\.tcl.view.tree")
    rows = tcleval(".view.tree size")
    test.verify(cast(rows, int) == expected_rows)
function verifyRows(expected_rows)
{
    waitForObject(":addressbook\\.tcl.view.tree");
    var rows = tcleval(".view.tree size");
    test.verify(parseInt(rows) == expected_rows);
}
sub verifyRows
{
    my $expected_rows = shift;
    waitForObject(":addressbook\\.tcl.view.tree");
    my $rows = tcleval(".view.tree size");
    test::verify($rows eq $expected_rows);
}
def verifyRows(expected_rows)
    waitForObject(":addressbook\\.tcl.view.tree")
    rows = tcleval(".view.tree size")
    Test.compare(String(rows), String(expected_rows))
end

Rather than duplicate the three lines needed to verify the row count in two separate places we have packaged the functionality up into a tiny function. For the Python version, we used the Object cast(object, type) function since Squish has its own int object; see also, Squish's Python Modules.

proc addNameAndAddress {oneNameAndAddress} {
    invokeMenuItem "Edit" "Add..."
    set fieldNames [list "forename" "surname" "phone" "email"]
    for {set field 0} {$field < [llength $fieldNames]} {incr field} {
        set fieldName [lindex $fieldNames $field]
        set text [lindex $oneNameAndAddress $field]
        invoke type [waitForObject ":addressbook\\.tcl.dialog.$fieldName"] $text
    }
    invoke clickButton [waitForObject ":addressbook\\.tcl.dialog.buttonarea.ok"]
}
def addNameAndAddress(oneNameAndAddress):
    invokeMenuItem("Edit", "Add...")
    for fieldName, text in zip(("forename", "surname", "phone", "email"), oneNameAndAddress):
        type(waitForObject(":addressbook\\.tcl.dialog.%s" % fieldName), text)
    clickButton(waitForObject(":addressbook\\.tcl.dialog.buttonarea.ok"))
function addNameAndAddress(oneNameAndAddress)
{
    invokeMenuItem("Edit", "Add...");
    var fieldNames = new Array("forename", "surname", "phone", "email");
    for (var i = 0; i < oneNameAndAddress.length; ++i) {
        var fieldName = fieldNames[i];
        var text = oneNameAndAddress[i];
        type(waitForObject(":addressbook\\.tcl.dialog." + fieldName), text);
    }
    clickButton(waitForObject(":addressbook\\.tcl.dialog.buttonarea.ok"));
}
sub addNameAndAddress
{
    my (@oneNameAndAddress) = @_;
    invokeMenuItem("Edit", "Add...");
    my @fieldNames = ("forename", "surname", "phone", "email");
    my $fieldName = "";
    for (my $i = 0; $i < scalar(@fieldNames); $i++) {
        $fieldName = $fieldNames[$i];
        my $text = $oneNameAndAddress[$i];
        type(waitForObject(":addressbook\\.tcl.dialog.$fieldName"), $text);
    }
    clickButton(waitForObject(":addressbook\\.tcl.dialog.buttonarea.ok"));
}
def addNameAndAddress(oneNameAndAddress)
    invokeMenuItem("Edit", "Add...")
    ["forename", "surname", "email", "phone"].each_with_index do
        |fieldName, index|
        text = oneNameAndAddress[index]
        type(waitForObject(":addressbook\\.tcl.dialog.#{fieldName}"), text)
    end
    clickButton(waitForObject(":addressbook\\.tcl.dialog.buttonarea.ok"))
end

For each set of name and address data we invoke the Edit > Add menu option to pop up the Add dialog. Then for each value received, we populate the appropriate field by waiting for the relevant line edit to be ready and then typing in the text using the type(objectName, text) function. We got the type(objectName, text) function call by copying it from the recorded tst_general test and parametrizing it by the field name and the text. And at the end, we click the dialog's OK, again using code we copied from the recorded tst_general test case.

proc closeWithoutSaving {} {
    invokeMenuItem "File" "Quit"
    invoke clickButton [waitForObject ":addressbook\\.tcl.__tk__messagebox.no"]
}
def closeWithoutSaving():
    invokeMenuItem("File", "Quit")
    clickButton(waitForObject(":addressbook\\.tcl.__tk__messagebox.no"))
function closeWithoutSaving()
{
    invokeMenuItem("File", "Quit");
    clickButton(waitForObject(":addressbook\\.tcl.__tk__messagebox.no"));
}
sub closeWithoutSaving
{
    invokeMenuItem("File", "Quit");
    clickButton(waitForObject(":addressbook\\.tcl.__tk__messagebox.no"));
}
def closeWithoutSaving
    invokeMenuItem("File", "Quit")
    clickButton(waitForObject(":addressbook\\.tcl.__tk__messagebox.no"))
end

Here we use the invokeMenuItem to do File > Quit, and then click the Save unsaved changes? dialog's No. The last line was copied from the recorded test.

The entire test is around 75 lines of code—and would be even less if we moved the common functions (such as invokeMenuItem, enterText, verifyRows, and closeWithoutSaving) into a shared script. And much of the code was copied directly from the recorded test, and in some cases parametrized.

This should be sufficient to give a flavor of writing test scripts for an AUT. Keep in mind that Squish provides far more functionality than we used here, (all of which is covered in the API Reference and the Tools Reference). And Squish also provides access to the entire public APIs of the AUT's objects.

However, one aspect of the test case is not very satisfactory. Although embedding test data as we did here is sensible for small amounts, it is rather limiting, especially when we want to use a lot of test data. Also, we didn't test any of the data that was added to see if it correctly ended up in the table. In the next section we will create another version of this test, only this time we will pull in the data from an external data source, and check that the actual data we added to the table is correct.

Creating Data Driven Tests

In the previous section we put three hard-coded names and addresses in our test. But what if we want to test lots of data? Or what if we want to change the data without having to change our test script's source code. One approach is to import a dataset into Squish and use the dataset as the source of the values we insert into our tests. Squish can import data in .tsv (tab-separated values format), .csv (comma-separated values format), .xls or .xlsx (Microsoft Excel spreadsheet formats).

Note: Both .csv and .tsv files are assumed to use the Unicode UTF-8 encoding, which is used for all test scripts.

Test data can be imported using the Squish IDE, or manually using a file manager or console commands. We will describe both approaches, starting with using the Squish IDE.

For the addressbook.tcl application we want to import the MyAddresses.tsv data file. To do this, we must start by clicking File > Import Test Resource to pop-up the Import Squish Resource dialog. Inside the dialog, click the Browse button to choose the file to import—in this case MyAddresses.tsv. Make sure that the Import As combobox is set to "TestData". By default the Squish IDE will import the test data just for the current test case, but we want the test data to be available to all the test suite's test cases: to do this check the Copy to Test Suite for Sharing radio button. Now click the Finish button. You can now see the file listed in the Test Suite Resources view (in the Test Data tab), and if you click the file's name it will be shown in an Editor view. The screenshot shows Squish after the test data has been added.

To import test data from outside the Squish IDE, use a file manager, such as File Explorer or Finder, or console commands. Create a directory called shared inside the test suite's directory. Then, create a directory called testdata inside the shared directory. Copy the data file (in this example, MyAddresses.tsv) into the shared\testdata directory.

Restart the Squish IDE if it is running. If you click the Test Suite Resources view's Test Data tab, you should see the data file. Click the file name to see the file in an Editor view.

"Squish with some imported test data"

Although in real life we would modify our tst_adding test case to use the test data, for the purpose of the tutorial we will make a new test case called tst_adding_data that is a copy of tst_adding and which we will modify to make use of the test data.

The only function we have to change is main, where instead of iterating over hard-coded items of data, we iterate over all the records in the dataset. We also need to update the expected row count at the end since we are adding a lot more records now, and we will also add a function to verify each record that's added.

proc main {} {
    startApplication "addressbook.tcl"
    invokeMenuItem "File" "New"
    verifyRows 0
    # Set a limit to avoid testing 100s of rows
    set limit 10
    set data [testData dataset "MyAddresses.tsv"]
    set columns [llength [testData fieldNames [lindex $data 0]]]
    set row 0
    for {} {$row < [llength $data]} {incr row} {
        set record [lindex $data $row]
        set forename [testData field $record "Forename"]
        set surname [testData field $record "Surname"]
        set phone [testData field $record "Phone"]
        set email [testData field $record "Email"]
        set details [list $forename $surname $phone $email]
        addNameAndAddress $details
        checkNameAndAddress $record
        if {$row > $limit} {
            break
        }
    }
    verifyRows [expr $row + 1]
    closeWithoutSaving
}
def main():
    startApplication("addressbook.tcl")
    invokeMenuItem("File", "New")
    verifyRows(0)
    limit = 10 # To avoid testing 100s of rows since that would be boring
    for row, record in enumerate(testData.dataset("MyAddresses.tsv")):
        forename = testData.field(record, "Forename")
        surname = testData.field(record, "Surname")
        phone = testData.field(record, "Phone")
        email = testData.field(record, "Email")
        addNameAndAddress((forename, surname, phone, email)) # pass as a single tuple
        checkNameAndAddress(record)
        if row > limit:
            break
    verifyRows(row + 1)
    closeWithoutSaving()
function main()
{
    startApplication("addressbook.tcl");
    invokeMenuItem("File", "New");
    verifyRows(0);
    var limit = 10; // To avoid testing 100s of rows since that would be boring
    var records = testData.dataset("MyAddresses.tsv");
    for (var row = 0; row < records.length; ++row) {
        var record = records[row];
        var forename = testData.field(record, "Forename");
        var surname = testData.field(record, "Surname");
        var phone = testData.field(record, "Phone");
        var email = testData.field(record, "Email");
        addNameAndAddress(new Array(forename, surname, phone, email));
        checkNameAndAddress(record);
        if (row > limit)
            break;
    }
    verifyRows(row + 1);
    closeWithoutSaving();
}
sub main
{
    startApplication("addressbook.tcl");
    invokeMenuItem("File", "New");
    verifyRows(0);
    my $limit = 10; # To avoid testing 100s of rows since that would be boring
    my @records = testData::dataset("MyAddresses.tsv");
    my $row = 0;
    for (; $row < scalar(@records); ++$row) {
        my $record = $records[$row];
        my $forename = testData::field($record, "Forename");
        my $surname = testData::field($record, "Surname");
        my $phone = testData::field($record, "Phone");
        my $email = testData::field($record, "Email");
        addNameAndAddress($forename, $surname, $phone, $email);
        checkNameAndAddress($record);
        if ($row > $limit) {
            last;
        }
    }
    verifyRows($row + 1);
    closeWithoutSaving;
}
def main
    startApplication("addressbook.tcl")
    invokeMenuItem("File", "New")
    verifyRows(0)
    limit = 10 # To avoid testing 100s of rows since that would be boring
    rows = 0
    TestData.dataset("MyAddresses.tsv").each_with_index do
        |record, row|
        forename = TestData.field(record, "Forename")
        surname = TestData.field(record, "Surname")
        email = TestData.field(record, "Email")
        phone = TestData.field(record, "Phone")
        addNameAndAddress([forename, surname, email, phone]) # pass as a single Array
        checkNameAndAddress(record)
        break if row > limit
        rows += 1
    end
    verifyRows(rows + 1)
    closeWithoutSaving
end

Squish provides access to test data through its testData module's functions—here we used the Dataset testData.dataset(filename) function to access the data file and make its records available, and the String testData.field(record, fieldName) function to retrieve each record's individual fields.

Having used the test data to populate table we want to be confident that the data in the table is the same as what we have added, so that's why we added the checkNameAndAddress function. We also added a limit to how many records we would compare, just to make the test run faster.

proc checkNameAndAddress {record} {
    set columns [llength [testData fieldNames $record]]
    for {set column 0} {$column < $columns} {incr column} {
        set expected_text [testData field $record $column]
        waitForObject ":addressbook\\.tcl.view.tree"
        # New items are always inserted before the current one, so the row is always 0
        set cell [toString [invoke tcleval ".view.tree cellindex 0,$column"]]
        set actual_text [invoke tcleval ".view.tree cellcget $cell -text"]
        test compare $expected_text $actual_text
    }
}
def checkNameAndAddress(record):
    for column in range(len(testData.fieldNames(record))):
        expected_text = testData.field(record, column)
        waitForObject(":addressbook\\.tcl.view.tree")
        # New items are always inserted before the current one, so the row is always 0
        actual_text = tcleval(".view.tree cellcget [.view.tree cellindex 0,%d] -text" % column)
        test.compare(expected_text, actual_text)
function checkNameAndAddress(record)
{
    for (var column = 0; column < testData.fieldNames(record).length; ++column) {
        var expected_text = testData.field(record, column);
        waitForObject(":addressbook\\.tcl.view.tree");
        var actual_text = tcleval(".view.tree cellcget [.view.tree cellindex 0," + column + "] -text");
        test.compare(expected_text, actual_text);
    }
}
sub checkNameAndAddress
{
    my $record = shift;
    my @columnNames = testData::fieldNames($record);
    for (my $column = 0; $column < scalar(@columnNames); $column++) {
        my $expected_text = testData::field($record, $column);
        waitForObject(":addressbook\\.tcl.view.tree");
        # New items are always inserted before the current one, so the row is always 0
        my $actual_text = tcleval(".view.tree cellcget [.view.tree cellindex 0,$column] -text");
        test::compare($expected_text, $actual_text);
    }
}
def checkNameAndAddress(record)
    for column in 0...TestData.fieldNames(record).length
        actual_text = tcleval(
        ".view.tree cellcget [.view.tree cellindex 0,#{column}] -text")
        Test.compare(actual_text, TestData.field(record, column))
    end
end

This function accesses the table's first row and extracts each of its columns' values. We use Squish's SequenceOfStrings testData.fieldNames(record) function to get a column count and then use the Boolean test.compare(value1, value2) function to check that each value in the table is the same as the value in the test data we used. Note that for this particular test we always insert new rows at the start of the table. The effect of this is that every new name and address is always added as the first row, so this is why we hard-coded the row to be 0.

The screenshot shows Squish's Test Summary log after the data-driven tests have been run.

"Squish after a successful data-driven test run"

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 User Guide provides 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. It is well worth reading the User Guide and at least skimming the API Reference and Tools Reference, especially since the time 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.

Further Tk example applications and their corresponding tests are provided in SQUISHDIR/examples/tk.