Thursday, November 5, 2009

Creating Executable Specifications for Swing apps with Concordion and FEST

The FEST Swing Module provides an easy-to-use API for testing Java applications that use the Swing GUI toolkit.

For a recent project, we created executable specifications with Concordion that used FEST as a driver for the desktop Swing-based GUI. The combination was very powerful, resulting in readable specifications of the system behaviour that invoked end-to-end tests through the GUI.

Here are some tips on using FEST effectively with Concordion:

1) Use FEST's built-in checks that all of the GUI updates are performed in the Event Dispatch Thread (EDT):

@BeforeClass
public static void setUpEDTChecks() {
FailOnThreadViolationRepaintManager.install();
}


2) Install a big red switch. When running the tests locally, they're going to take over your desktop. Use FEST's EmergencyAbortListener to allow you to terminate the GUI using ctrl - shift - A:

@BeforeClass
public static void setUpAbortListener() {
listener = EmergencyAbortListener.registerInToolkit();
}

@AfterClass
public static void tearDownAbortListener() {
listener.unregister();
}


3) Create your user interface once for all tests:

public static UserInterface getInstance() {
if (instance == null) {
instance = new UserInterface();
}
return instance;
}


4)) Create your window within the Event Dispatching Thread (EDT)

private FrameFixture window;

private UserInterface() {
GuiActionRunner.execute(new GuiTask() {
public void executeInEDT() {
JFrame frame = ... // code to create main frame
window = new FrameFixture(frame);
setLookAndFeel();
}
});
window.show();


5) Slow the tests down when demonstrating to users, so they can see what's happening:

public void slowDown() {
window.robot.settings().delayBetweenEvents(500);
window.robot.settings().eventPostingDelay(500);
}


6) When using FEST's assertions, you can allow assertion failures to bubble up to Concordion as AssertionErrors. However, you will get more readable Concordion output if you translate them. For example, with the following Concordion specification:

<p>Only things of type 'Thing One' should be prioritised....</p>
<table concordion:execute="#prioritised =
isPrioritised(#thingType)">
<tr><th concordion:set="#thingType">Thing Type</th>
<th concordion:assertEquals="#prioritised">Prioritised</th></tr>
<tr><td>Thing One</td><td>Yes</td></tr>
<tr><td>Thing Two</td><td>No</td></tr>
</table>


, the corresponding fixture can be written as:

public String isPrioritised(String thingType) {
doStuffWithThing(thingType);
return userInterface.isPrioritised(request) ? "Yes" : "No";
}


, and integrated with Fest using:

public Boolean isPrioritised(Thing thing) {
int rowIndex = getRowIndex(thing);
try {
table.fontAt(row(rowIndex).column(col)).requireBold();
} catch (AssertionError e) {
return false;
}
return true;
}


7) Follow all of the usual hints and tips for creating Concordion specifications. In particular:

  • Write Specifications not Scripts.

  • Don't mention the GUI. Your specification should be independent of the implementation.

  • Evolve a domain-specific language



I'll write a follow-up to describe how we got these tests running in our Continuous Integration server.
Post a Comment