UI Testing with JFCUnit

Purpose

Automated testing is an integral part of every serious enterprise application and should cover business tier logic as well as presentation tier logic. This contribution allows to write functional presentation tier tests using a thin abstraction layer on top of JFCUnit.

Related to

This contribution is related to UI Testing with Jemmy, which uses Jemmy as the underlying Swing testing framework.

How to use

Basics

Subclass BaseULCTestCase and implement the abstract launch() method. Typically, you will launch the ULC application to test using the TestingDevelopmentRunner.

protected void launch() {
  TestingDevelopmentRunner.setApplicationClass(ULCTestingSample.class);
  TestingDevelopmentRunner.main(new String[0]);
}

Implement your test cases using standard JUnit conventions, e.g. by implementing testXXX() methods (note that each such test method will launch a new ULC application instance). Within such a test method, trigger the simulated user actions and make appropriate assertions.

public void testSimple() {
  JFrame frame = findFrame(".*ui .*sample.*");

AbstractButton button = findButton(frame, "proceedButton"); assertEquals("disabled until text entered", false, button.isEnabled());

JTextField firstNameTextField = findTextField(frame, "firstName"); setText(firstNameTextField, "Homer");

JTextField lastNameTextField = findTextField(frame, "lastName"); setText(lastNameTextField, "Simpson");

assertEquals("enabled after text entered", true, button.isEnabled()); clickButton(button);

… }

In addition to the convenience methods provided in the BaseULCTestCase class, you can add your own convenience methods for widget lookup, widget manipulation, etc. Of course, you can use the JFCUnit classes directly, as well.

I recommend that you assign a unique name to each ULC component that needs to be accessed from test cases. This will be the most reliable approach to identify widgets.

…
fFirstNameTextField = new ULCTextField();
fFirstNameTextField.setName("firstName");
...

Advanced

Sometimes, while you are in a test case, it is necessary to execute code in the server-side ULC thread. This can be achieved by wrapping the code in a Runnable and have it be executed through TestingDevelopmentRunner.triggerServerSideCode(Runnable).

TestingDevelopmentRunner.triggerServerSideCode(new Runnable() {
  public void run() {
    System.out.println(Thread.currentThread());
  }
});
sync();

In order for this to work, your IApplication class must handle a specific message in handleMessage().

public void handleMessage(String inMessage) {
  if(TestingDevelopmentRunner.EXEC_SERVER_SIDE_CODE_MSG.equals(inMessage)) {
    TestingDevelopmentRunner.executeServerSideCode();
  }
  …
}

How to run the sample

Downlaod and extract the .zip file and call ant run.sample to run the sample and call ant run.test to run the automated test. The distribution comes with an IDEA module file, making it easy to run the SimpleSampleULCTestCase class from within IDEA.

How it is implemented

The testing functionality is available as a thin layer on top of JFCUnit. JFCUnit operates on the actual client-side Swing components created by the ULC application. Convenience methods are provided in the BaseULCTestCase class shielding the developer from the JFCUnit complexity. Each time a ULC test case gets executed, the ULC application is first launched in a separate thread.

Limitations

Looking at the source code and actually using the code, you will notice that there are still some rough edges:
  • After each triggered UI action, a delay is enforced through syncDelay(). This has been introduced because UISession.waitForIdle() does not always wait long enough, as experience revealed.
  • After running a test case, the process might not exit. This is due to some non-daemon threads still running.
  • When simulating data entry in a text field, sometimes, the first letter gets swallowed which made a work-around necessary. Possibly, this has to do with an IDataType being attached to a text field.

Resources