Avoiding Swing Threading Problems

Problem

You want to test Swing functions that dispatch to the AWT event queue, such as focus traversal.

Solution

Write a utility method to wait until pending AWT event queue messages are processed.

Discussion

Suppose you want to test the focus traversal order of the PersonEditorPanel introduced in the previous recipe. You want to ensure that focus travels from component to component in the correct order as the user hits the tab key. To do this, you write the following test:

public void testTabOrder(  ) {
    JTextField firstNameField = this.tannerPanel.getFirstNameField(  );

    firstNameField.requestFocusInWindow(  );

    // simulate the user hitting tab
    firstNameField.transferFocus(  );

    // ensure that the last name field now has focus
    JTextField lastNameField = this.tannerPanel.getLastNameField(  );
    assertTrue("Expected last name field to have focus",
            lastNameField.hasFocus(  ));
}

As written, this test fails. First and foremost, the components must be visible on screen before they can obtain focus. So you try modifying your setUp( ) method as follows:

protected void setUp(  ) throws Exception {
    this.emptyPanel = new PersonEditorPanel(  );

    this.tanner = new Person("Tanner", "Burke");
    this.tannerPanel = new PersonEditorPanel(  );
    this.tannerPanel.setPerson(this.tanner);

    getTestFrame().getContentPane(  ).add(this.tannerPanel,
        BorderLayout.CENTER);
               getTestFrame().pack(  );
               getTestFrame().show(  );
}

This takes advantage of the JFrame provided by our base class, SwingTestCase. When you run your test again, it still fails! The initial focus never made it to the first name field. Here is a partial solution:

public void testTabOrder(  ) {
    JTextField firstNameField = this.tannerPanel.getFirstNameField(  );

    // make sure the first name field has focus
               while (!firstNameField.hasFocus(  )) {
               getTestFrame().toFront(  );
               firstNameField.requestFocusInWindow(  );
               }

    // simulate the user hitting tab
    firstNameField.transferFocus(  );

    // ensure that the last name field now has focus
    JTextField lastNameField = this.tannerPanel.getLastNameField(  );
    assertTrue("Expected last name field to have focus",
            lastNameField.hasFocus(  ));
}

This approach keeps trying until the first name field eventually gains focus. It also brings the test frame to the front of other windows because, during testing, we found that the frame sometimes gets buried if the user clicks on any other window while the test is running. We discovered this by repeating our tests and clicking on other applications while the tests ran:

public static Test suite(  ) {
    return new RepeatedTest(
            new TestSuite(TestPersonEditorPanel.class), 1000);
}

We still have one more problem. When the test runs repeatedly, you will notice that the test fails intermittently. This is because the transferFocus( ) method does not occur immediately. Instead, the request to transfer focus is scheduled on the AWT event queue. In order to pass consistently, the test must wait until the event has a chance to be processed by the queue. Example 4-15 lists the final version of our test.

Example 4-15. Final tab order test

public void testTabOrder(  ) {
    JTextField firstNameField = this.tannerPanel.getFirstNameField(  );

    // make sure the first name field has focus
    while (!firstNameField.hasFocus(  )) {
        getTestFrame().toFront(  );
        firstNameField.requestFocusInWindow(  );
    }

    // simulate the user hitting tab
    firstNameField.transferFocus(  );

    // wait until the transferFocus(  ) method is processed
    waitForSwing(  );

    // ensure that the last name field now has focus
    JTextField lastNameField = this.tannerPanel.getLastNameField(  );
    assertTrue("Expected last name field to have focus",
            lastNameField.hasFocus(  ));
}

The waitForSwing( ) method is a new feature of our base class, SwingTestCase, that blocks until pending AWT events like transferFocus( ) are processed:

public void waitForSwing(  ) {
    if (!SwingUtilities.isEventDispatchThread(  )) {
        try {
            SwingUtilities.invokeAndWait(new Runnable(  ) {
                public void run(  ) {
                }
            });
        } catch (InterruptedException e) {
        } catch (InvocationTargetException e) {
        }
    }
}

Now, our test runs almost 100% of the time. When repeating the test thousands of times, you can make the test fail every once in a while by randomly clicking on other applications. This is because you take away focus just before the test asks the last name field if it has focus. This sort of problem is unavoidable, and illustrates one of the reasons why you should minimize your dependency on Swing tests.

The most valuable lesson of this recipe is the technique of repeating your graphical tests many thousands of times until all of the quirky Swing threading issues are resolved. Once your tests run as consistently as possible, remove the repeated test. Also, while your tests are running, avoid clicking on other running applications so you don’t interfere with focus events.

See Also

Chapter 11 provides some references to Swing-specific testing tools.

Get Java Extreme Programming Cookbook now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.