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.
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
. BecauseJTable
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.
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.