Testing Swing Code

Problem

You want to write unit tests for Swing portions of your application.

Solution

Keep application logic separate from GUI layout, thus minimizing the need to test graphical code directly. Also, design your user interface in terms of discrete components that are testable without complex setup and configuration.

Discussion

Graphical code presents many testing challenges. For instance, many Swing functions only work when the components are visible on screen. In these cases, your tests have to create dummy frames and show the components before the tests can succeed. In other cases, Swing schedules events on the AWT event queue rather than updating component states immediately. We show how to tackle this issue in the next recipe.

Ideally, you should strive to minimize the need to test Swing code in the first place. Application logic, such as computing the monthly payment amount for a loan, should not be intertwined with the JTable that displays the payment history. Instead, you might want to define three separate classes:

Loan

A utility class that keeps track of payments, interest rates, and other attributes. This class can be tested independently of Swing.

LoanPaymentTableModel

A Swing table model for a history of loan payments. Because table models are nongraphical, you can test them just like any other Java class.

JTable

Displays the LoanPaymentTableModel. Because JTable is provided with Swing, you don’t have to test it.

There are more complex scenarios where you cannot avoid Swing testing. Let’s suppose you need a panel to display information about a person and would like to test it. The Person class is easily testable on its own, and probably contains methods to retrieve a name, address, SSN, and other key pieces of information. But the PersonEditorPanel is graphical and a little more challenging to test. You might start with the code shown in Example 4-12.

Example 4-12. First draft of PersonEditorPanel.java

public class PersonEditorPanel extends JPanel {
    private JTextField firstNameField = new JTextField(20);
    private JTextField lastNameField = new JTextField(20);
    // @todo - add more fields later

    private Person person;

    public PersonEditorPanel(  ) {
        layoutGui(  );
        updateDataDisplay(  );
    }

    public void setPerson(Person p) {
        this.person = person;
        updateDataDisplay(  );
    }

    public Person getPerson(  ) {
        // @todo - update the person with new information from the fields
        return this.person;
    }

    private void layoutGui(  ) {
        // @todo - define the layout
    }

    private void updateDataDisplay(  ) {
        // @todo - ensure the fields are properly enabled, also set
        //         data on the fields.
    }
}

Our PersonEditorPanel does not function yet, but it is far enough along to begin writing unit tests. Before delving into the actual tests, let’s look at a base class for Swing tests. Example 4-13 shows a class that provides access to a JFrame for testing purposes. Our unit test for PersonEditorPanel will extend from SwingTestCase.

Example 4-13. SwingTestCase.java

package com.oreilly.javaxp.junit;

import junit.framework.TestCase;

import javax.swing.*;
import java.lang.reflect.InvocationTargetException;

public class SwingTestCase extends TestCase {
    private JFrame testFrame;

    protected void tearDown(  ) throws Exception {
        if (this.testFrame != null) {
            this.testFrame.dispose(  );
            this.testFrame = null;
        }
    }

    public JFrame getTestFrame(  ) {
        if (this.testFrame == null) {
            this.testFrame = new JFrame("Test");
        }
        return this.testFrame;
    }
}

SwingTestCase provides access to a JFrame and takes care of disposing the frame in its tearDown( ) method. As you write more Swing tests, you can place additional functionality in SwingTestCase.

Example 4-14 shows the first few tests for PersonEditorPanel. In these tests, we check to see if the fields in the panel are enabled and disabled properly.

Example 4-14. The first PersonEditorPanel tests

public class TestPersonEditorPanel extends SwingTestCase {
    private PersonEditorPanel emptyPanel;
    private PersonEditorPanel tannerPanel;
    private Person tanner;

    protected void setUp(  ) throws Exception {
        // create a panel without a Person
        this.emptyPanel = new PersonEditorPanel(  );

        // create a panel with a Person
        this.tanner = new Person("Tanner", "Burke");
        this.tannerPanel = new PersonEditorPanel(  );
        this.tannerPanel.setPerson(this.tanner);
    }

    public void testTextFieldsAreInitiallyDisabled(  ) {
        assertTrue("First name field should be disabled",
                !this.emptyPanel.getFirstNameField().isEnabled(  ));
        assertTrue("Last name field should be disabled",
                !this.emptyPanel.getLastNameField().isEnabled(  ));
    }

    public void testEnabledStateAfterSettingPerson(  ) {
        assertTrue("First name field should be enabled",
                this.tannerPanel.getFirstNameField().isEnabled(  ));
        assertTrue("Last name field should be enabled",
                this.tannerPanel.getLastNameField().isEnabled(  ));
    }

You might notice that our tests have to get to the first and last name fields, so we need to introduce the getFirstNameField( ) and getLastNameField( ) methods in our panel:

JTextField getFirstNameField(  ) {
    return this.firstNameField;
}

JTextField getLastNameField(  ) {
    return this.lastNameField;
}

These methods are package-scope because we only need them for testing purposes. When you first run the unit tests, they will fail because we did not write any logic to enable and disable the fields. This method can be added to PersonEditorPanel in order to make the tests pass:

private void updateEnabledStates(  ) {
    this.firstNameField.setEnabled(person != null);
    this.lastNameField.setEnabled(person != null);
}

Once you get these tests working, you can test for the actual values of the two fields:

public void testFirstName(  ) {
    assertEquals("First name", "",
            this.emptyPanel.getFirstNameField().getText(  ));
    assertEquals("First name", this.tanner.getFirstName(  ),
            this.tannerPanel.getFirstNameField().getText(  ));
}

public void testLastName(  ) {
    assertEquals("Last name", "",
            this.emptyPanel.getLastNameField().getText(  ));
    assertEquals("Last name", this.tanner.getLastName(  ),
            this.tannerPanel.getLastNameField().getText(  ));
}

These will also fail until you add some more logic to PersonEditorPanel to set data on the two text fields:

private void updateDataDisplay(  ) {
    if (this.person == null) {
        this.firstNameField.setText("");
        this.lastNameField.setText("");
    } else {
        this.firstNameField.setText(this.person.getFirstName(  ));
        this.lastNameField.setText(this.person.getLastName(  ));
    }
    updateEnabledStates(  );
}

When complete, your tests should confirm that you can create an empty panel, set a person object on it, and retrieve person object after it has been edited. You should also write tests for unusual conditions, such as a null person reference or null data within the person. This is a data-oriented test, ensuring that the panel properly displays and updates its data. We did not try to verify the graphical positioning of the actual components, nor have we tried to test user interaction with the GUI.

See Also

Recipe 4.19 discusses problems with java.awt.Robot. Chapter 11 provides some references to Swing-specific testing tools. Recipe 11.6 discusses some pros and cons of making methods package-scope for the sole purpose of testing them.

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.