|
|
|
|
Developing Swing components while staying on the path of test driven development has no doubt challenged many seasoned Swing developers. Many aspects of GUI testing are indeed not that complicated as long as one adopts the simplicity principle. Moreover, there are quite a few Swing testing frameworks that make it easier to test Swing applications - Abbot and Jemmy being amongst the most popular. This article focuses on developing a couple of custom Swing GUI components using test driven techniques. We will also develop a simple GUI testing helper class that will aid us in testing our custom components. This article does not cover any GUI testing framework and the reader is advised to look them up elsewhere.
A searchable list box provides capability to search a list box for a particular item. The user enters some text in the provided search box. For each keystroke, the list box automatically scrolls down and selects the closest match it has found so far. If a match is not found, the list box does not select anything. The searchable list box is shown in Fig 1.
|
|
| Figure 1. Searchable List Box | Figure 2. Searchable List Box With "United S" typed |
Keeping a simple approach to development, the intial coding provides a simple SearchableListBox that extends JPanel and is composed of three sub-components: a JLabel that displays a custom text like "Search" or "Find", a JTextField of default length 30 that allows the user to type in the search text, and a list box that contains the list of items that could be searched. There is no functionality at this point other than the basic layout of the components. A simple unit test using the JUnit framework ( http://www.junit.org ) tests the basic display of the panel. There are no asserts yet - just a visual check. The display is shown in Figure 1 above.
/*
* Author: Santosh Shanbhag
*/
package com.ociweb.testinggui;
import junit.framework.TestCase;
import javax.swing.*;
public class SearchableListBox_UT extends TestCase {
// String based test data that is used to populate the list items
public static final String[] LIST_DATA = new String[]{
"United Kingdom", "Italy", "India", "Uganda", "Austraila",
"United States", "Austria", "Indonesia"};
public SearchableListBox_UT(String name) {
super(name);
}
/**
* Initial Test to check display of components.
* This test is useful only initially when we are designing the component. It helps us to verify
* visually that the screen layout of the component is proper. In an automated test environment, it is
* advisable to comment out this test before commiting the code
*
*/
public void testDisplay() throws Exception {
JFrame frame = new JFrame();
SearchableListBox searchableListBox = new SearchableListBox(LIST_DATA);
frame.getContentPane().add(searchableListBox);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
// sleep for 60 seconds so that you can check the display
Thread.sleep(60000);
}
}
/*
* Author: Santosh Shanbhag
*/
package com.ociweb.testinggui;
import javax.swing.*;
import java.awt.*;
public class SearchableListBox extends JPanel {
// default search label text
public static final String SEARCH_LABEL_DEFAULT_TEXT = "Search:";
// default search field length
public static final int SEARCH_FIELD_DEFAULT_LENGTH = 30;
private JLabel searchLabel;
private JTextField searchField;
private JList list;
/**
* Constructor.
* @param listData The list box items
*/
public SearchableListBox(Object[] listData) {
init(this.searchLabelTxt, this.searchFieldLength, listData);
}
/**
* Constructor.
* @param searchLabelTxt
* The text used to display above the search field. For example: "Search", "Find", "Get it"
* @param searchFieldLength
* The length of the search field
* @param listData
* The list box items
*/
public SearchableListBox(String searchLabelTxt, int searchFieldLength, Object[] listData) {
init(searchLabelTxt, searchFieldLength, listData);
}
/**
* Initialization routine to create GUI components.
* @param searchLabelTxt
* The text used to display above the search field. For example: "Search", "Find", "Get it"
* @param searchFieldLength
* The length of the search field
* @param listData
* The list box items
*/
private void init(String searchLabelTxt, int searchFieldLength, Object[] listData) {
searchLabel = new JLabel(searchLabelTxt);
searchField = new JTextField(searchFieldLength);
list = new JList(listData);
// we would like to see the first ten items
list.setVisibleRowCount(10);
// limit the selection to a single item
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
layoutComponents();
}
/**
* Lays out GUI components on the screen.
*/
private void layoutComponents() {
setLayout(new BorderLayout());
// create a search panel to contain search label and
// search field.
JPanel searchPanel = new JPanel(new BorderLayout());
searchPanel.add(searchLabel, BorderLayout.NORTH);
searchPanel.add(searchField, BorderLayout.SOUTH);
// put list box in the scroll pane
JScrollPane scrollpaneForList = new JScrollPane(list,
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
add(searchPanel, BorderLayout.NORTH);
add(scrollpaneForList, BorderLayout.CENTER);
}
}
At this point we are probably satisfied with the basic display and are ready to implment the search functionality. However, we still need an additional test that will verify if anything breaks in the basic construction of the component. So we add another test to the UT class as shown below. We also have a GUITestHelper class that has static methods to help us with the testing. The code for the GUITestHelper is provided at the end of this article. We still have no way of finding out if the components are laid out correctly but rely instead on the visual test.
public void testConstruction() throws Exception {
// for testing, use "Find" text instead of the default "Search"
String searchLabelTxt = "Find";
int searchFieldLength = 20;
SearchableListBox searchableListBox = new SearchableListBox(searchLabelTxt, searchFieldLength, LIST_DATA);
// find out if a JLabel with "Find" text is contained in the custom component
JLabel searchLabel = GUITestHelper.findJLabelContainingText(searchableListBox, searchLabelTxt);
assertNotNull("Could not find a JLabel with text: " + searchLabelTxt, searchLabel);
// find out if a JTextField of length 20 is contained in the custom component
JTextField textField = GUITestHelper.findJTextFieldOfLength(searchableListBox, searchFieldLength);
assertNotNull("Could not find a JTextField of length: " + searchFieldLength, textField);
// test that the list contains same number of items that were passed in
assertEquals(LIST_DATA.length, searchableListBox.getItemCount());
// As of now, test that the list maintains the order of the items that were passed in.
// But in the future, if someone sorts the order of the items in the list, this test
// will need to be updated. At that time, the updated test will ensure that the code
// that keeps the list items in a sorted order is not broken.
for (int i = 0; i < LIST_DATA.length; i++) {
String expectedListItem = LIST_DATA[i];
assertEquals("List Item not in the order it was passed in",
expectedListItem, searchableListBox.getItemAt(i));
}
}
The next step is to test and implement the search functionality. The JTextField stores it's data
in a model whose class is javax.swing.text.Document. A document listener object (javax.swing.event.DocumentListener) can be associated with the Document that intercepts
insertions, deletions, and changes made to it. We define our DocumentListener class as
shown below:
/*
* Custom document listener class that is used by SearchableListBox to intercept text field changes.
* Just to make sure all implemented methods call doUpdate().
*
* Author: Santosh Shanbhag
*/
package com.ociweb.testinggui;
import javax.swing.event.DocumentListener;
public abstract class CustomDocumentListener implements DocumentListener {
public void changedUpdate(DocumentEvent e) {
doUpdate();
}
public void insertUpdate(DocumentEvent e) {
doUpdate();
}
public void removeUpdate(DocumentEvent e) {
doUpdate();
}
public abstract void doUpdate();
}
Now all we have to do is plug this document listener to the JTextField's
Document model and implement the doUpdate()
method. The
doUpdate()
method will implement the search functionality. We create an inner class
in SearchableListBox that extends CustomDocumentListener so that the listener
has access to both the search text field and the list box. This is shown in the code
below:
private void init(String searchLabelTxt, int searchFieldLength, Object[] listData) {
this.listData = listData;
list = new JList(listData);
searchLabel = new JLabel(searchLabelTxt);
searchField = new JTextField(searchFieldLength);
// we would like to see the first ten items
list.setVisibleRowCount(10);
// limit the selection to a single item
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
// add a document listener to the text field
SearchFieldDocumentListener searchFieldListener = new SearchFieldDocumentListener();
searchField.getDocument().addDocumentListener(searchFieldListener);
// lay out GUI components
layoutComponents();
}
// returns the currently selected item in the list box
public Object getSelectedItem() {
return list.getSelectedValue();
}
//******* inner class of SearchableListBox
class SearchFieldDocumentListener extends CustomDocumentListener {
public void doUpdate() {
doSearch();
}
}
The SearchFieldDocumentListener implements the
doUpdate()
method by making a call to the
doSearch()
method in the SearchableListBox
class. This keeps the listener code from refering any objects of
SearchableListBox directly thus reducing dependency between the two classes. Next we have to
write a test to make sure that
doSearch()
implementation is correct. This is
shown below:
public void testSearchImplementation() throws Exception {
String[] LIST_ITEMS = new String[]{
"United Kingdom", "Italy", "India", "Uganda", "Australia", "Chile", "Brazil", "United States", "Austria",
"Indonesia", "Indiana", "Oklahoma", "New Jersey", "New Hampshire", "Africa"};
SearchableListBox searchableListBox = new SearchableListBox(LIST_ITEMS);
// get a handle on the search text field
int searchFieldDefaultLength = SearchableListBox.SEARCH_FIELD_DEFAULT_LENGTH;
JTextField textField = GUITestHelper.findJTextFieldOfLength(searchableListBox, searchFieldDefaultLength);
assertNotNull("Could not find a JTextField of length: " + searchFieldDefaultLength, textField);
textField.setText("Austri");
searchableListBox.getSelectedItem();
assertEquals("Austria", searchableListBox.getSelectedItem());
textField.setText("Austra");
searchableListBox.getSelectedItem();
assertEquals("Australia", searchableListBox.getSelectedItem());
textField.setText("Af");
searchableListBox.getSelectedItem();
assertEquals("Africa", searchableListBox.getSelectedItem());
textField.setText("United");
searchableListBox.getSelectedItem();
assertEquals("United Kingdom", searchableListBox.getSelectedItem());
textField.setText("United K");
searchableListBox.getSelectedItem();
assertEquals("United Kingdom", searchableListBox.getSelectedItem());
textField.setText("United S");
searchableListBox.getSelectedItem();
assertEquals("United States", searchableListBox.getSelectedItem());
textField.setText("New");
searchableListBox.getSelectedItem();
assertEquals("New Jersey", searchableListBox.getSelectedItem());
textField.setText("New H");
searchableListBox.getSelectedItem();
assertEquals("New Hampshire", searchableListBox.getSelectedItem());
textField.setText("Ug");
searchableListBox.getSelectedItem();
assertEquals("Uganda", searchableListBox.getSelectedItem());
}
Finally the implementation of the
doSearch()
method is shown below:
private void doSearch() {
// find the closest matching item
int searchItemIndex = findSearchItemIndex(list, searchField.getText());
// clear any previous selections
list.clearSelection();
list.setSelectedIndex(searchItemIndex);
// scroll down to make sure the selected item is visible
list.ensureIndexIsVisible(Math.max(searchItemIndex, 0));
}
private int findSearchItemIndex(JList inputList, String searchString) {
// if there is nothing in the search text field just return -1
if ("".equals(searchString)) return -1;
int indexOfSearchedItem = -1;
for (int i = 0; i < listData.length; i++) {
String listItemString = listData[i].toString();
// if the search string is contained at the start of the list item string, we found it!!!
if (listItemString.indexOf(searchString) == 0) {
indexOfSearchedItem = i;
break;
}
}
// make sure we never go out of array bounds
return Math.min(indexOfSearchedItem, listData.length - 1);
}
Here we design and test a component that has two list boxes and two buttons. When the first button is clicked, the selected item from the first list box is transferred to the second. When the second button is clicked, the selected item from the second list box is transferred to the first. These are the basic requirements of this component. The component is shown in figure 3 below.
|
| Figure 3. Two List Boxes With Inter-Transferable Items |
Keeping a simple approach to development just like for SearchableListBox before,
the intial coding provides a simple component ListBoxesWithTransferableItems that extends
JPanel and is composed of six sub-components: two JLabels that display a
text for each list box, two JButtons one for transfer from left-to-right and vice-
versa, and two JLists. There is no functionality at this point other than the basic
layout of the components. A simple unit test using the JUnit framework tests the basic display of
the panel. There are no asserts yet - just a visual check. The display is shown in Figure 3 above.
/*
* UT class for testing ListBoxesWithTransferableItems.
*
* Author: Santosh Shanbhag
*/
package com.ociweb.testinggui;
import junit.framework.TestCase;
import javax.swing.*;
import java.awt.*;
public class ListBoxesWithTransferableItems_UT extends TestCase {
public ListBoxesWithTransferableItems_UT(String name) {
super(name);
}
/**
* Initial Test to check display of components.
* This test is useful only initially when we are designing the component. It helps us to verify
* visually that the screen layout of the component is proper. In an automated test environment, it is
* advisable to comment out this test before commiting the code
*
*/
public void testDisplay() throws Exception {
String[] LIST_DATA_LEFT = {"Apples", "Bananas", "Oranges", "Grapes", "WaterMelons", "HoneyDew", "Nectarines"};
String[] LIST_DATA_RIGHT = {"Peaches", "Cantaloupes", "Figs"};
ListBoxesWithTransferableItems listBoxesWithTransferableItems
= new ListBoxesWithTransferableItems(LIST_DATA_LEFT, LIST_DATA_RIGHT);
listBoxesWithTransferableItems.setBorder(BorderFactory.createLineBorder(Color.black));
// create a test frame to show the panel
JFrame frame = new JFrame();
frame.getContentPane().add(listBoxesWithTransferableItems);
frame.pack();
// center the frame on the screen
frame.setLocationRelativeTo(null);
frame.show();
// 1-minute delay to see the component
Thread.sleep(60000);
}
}
/*
* GUI Component that has two list boxes with items that can be transferred between them.
*
* Author: Santosh Shanbhag
*/
package com.ociweb.testinggui;
import javax.swing.*;
import java.awt.*;
public class ListBoxesWithTransferableItems extends JPanel {
// label that will be displayed above the left list box
private JLabel labelForLeftListBox = new JLabel("Left List Box");
// label that will be displayed above the right list box
private JLabel labelForRightListBox = new JLabel("Right List Box");
private JList leftListBox;
private JList rightListBox;
// on clicking, items will be transferred from left to right list box
private JButton transferFromLeftToRightButton = new JButton(">>");
// on clicking, items will be transferred from right to left list box
private JButton transferFromRightToLeftButton = new JButton("<<");
/**
* Constructor.
*
* @param listDataForLeftList - data for left side list box
* @param listDataForRightList - data for right side list box
*/
public ListBoxesWithTransferableItems(Object[] listDataForLeftList, Object[] listDataForRightList) {
init(listDataForLeftList, listDataForRightList);
}
/**
* Constructor.
*
* @param listDataForLeftList - data for left side list box
* @param listDataForRightList - data for right side list box
* @param leftListBoxLabelTxt - label text for left side list box
* @param rightListBoxLabelTxt - label text for right side list box
*/
public ListBoxesWithTransferableItems(Object[] listDataForLeftList, Object[] listDataForRightList,
String leftListBoxLabelTxt, String rightListBoxLabelTxt) {
labelForLeftListBox.setText(leftListBoxLabelTxt);
labelForRightListBox.setText(rightListBoxLabelTxt);
init(listDataForLeftList, listDataForRightList);
}
private void init(Object[] listDataForLeftList, Object[] listDataForRightList) {
leftListBox = new JList(listDataForLeftList);
rightListBox = new JList(listDataForRightList);
layoutComponents();
}
// use a layout manager to lay out sub components in the panel
private void layoutComponents() {
add(createPanelForListBoxWithLabel(leftListBox, labelForLeftListBox));
JPanel buttonPanel = new JPanel(new BorderLayout());
buttonPanel.add(transferFromLeftToRightButton, BorderLayout.NORTH);
buttonPanel.add(transferFromRightToLeftButton, BorderLayout.SOUTH);
add(buttonPanel);
add(createPanelForListBoxWithLabel(rightListBox, labelForRightListBox));
}
// create a panel with label displayed above the list box.
// put list box in a scroll pane.
private JPanel createPanelForListBoxWithLabel(JList listBox, JLabel label) {
JPanel panel = new JPanel(new BorderLayout());
panel.add(label, BorderLayout.NORTH);
JScrollPane scrollPaneForListBox = new JScrollPane(listBox);
panel.add(scrollPaneForListBox, BorderLayout.CENTER);
return panel;
}
}
The GUI has no functionality yet so we move on to the next step. The JButtons need
an ActionListener that will act on the list boxes (transfering an item from one to the
other) when clicked. Using an inner class as the listener we provide a method stub in the
ListBoxesWithTransferableItems class that will be called everytime a button is clicked. This
is shown in the code below (ListBoxesWithTransferableItems.java).
private void transferSelectedItemFromRightListBoxToLeft() {
}
private void transferSelectedItemFromLeftListBoxToRight() {
}
/**
* ******* Inner Class *************
*/
class ButtonActionListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
JButton sourceButton = (JButton) e.getSource();
if (sourceButton == transferFromLeftToRightButton) {
// transfer from left to right
transferSelectedItemFromLeftListBoxToRight();
}
else if (sourceButton == transferFromRightToLeftButton) {
// transfer from right to left
transferSelectedItemFromRightListBoxToLeft();
}
}
}
Since we are following test driven development, we should write a unit test at this point to verify that selected items will be transfered from one list box to the other when the appropriate buttons are clicked. Here's the test for transfering from left list to right list By making slight changes, we can easily write another test that verifies the transfer of items from right-to-left. The code is provided in the listing at the bottom.
public void testTransferItemsFromLeftListToRightList() throws Exception {
ListBoxesWithTransferableItems listBoxesWithTransferableItems
= new ListBoxesWithTransferableItems(LIST_DATA_LEFT, LIST_DATA_RIGHT);
JButton leftToRightTransferButton = GUITestHelper.findButtonWithText(listBoxesWithTransferableItems, ">>");
assertNotNull(leftToRightTransferButton);
// assert that left list contains expected data
Object[] leftListData = listBoxesWithTransferableItems.getAllItemsFromLeftListBox();
assertTrue(Arrays.equals(LIST_DATA_LEFT, leftListData));
// assert that right list contains expected data
Object[] rightListData = listBoxesWithTransferableItems.getAllItemsFromRightListBox();
assertTrue(Arrays.equals(LIST_DATA_RIGHT, rightListData));
// click the left-to-right transfer button
leftToRightTransferButton.doClick();
// there was nothing selected in left list, so nothing should be removed from left list
leftListData = listBoxesWithTransferableItems.getAllItemsFromLeftListBox();
assertTrue(Arrays.equals(LIST_DATA_LEFT, leftListData));
// there was nothing selected in left list, so nothing should be added to right list
rightListData = listBoxesWithTransferableItems.getAllItemsFromRightListBox();
assertTrue(Arrays.equals(LIST_DATA_RIGHT, rightListData));
listBoxesWithTransferableItems.selectItemInLeftListAtIndex(0);
leftToRightTransferButton.doClick();
assertEquals("Items in left list should have decreased by 1",
LIST_DATA_LEFT.length - 1, listBoxesWithTransferableItems.getAllItemsFromLeftListBox().length);
assertEquals("Items in left list should have increased by 1",
LIST_DATA_RIGHT.length + 1, listBoxesWithTransferableItems.getAllItemsFromRightListBox().length);
}
and here's the source code for the transfer from left-to-right!!!
private void transferSelectedItemFromLeftListBoxToRight() {
int selectedIndex = getSelectedIndexOfLeftListBox();
// return if no selected item
if (selectedIndex == -1) return;
Object selectedItemFromLeftListBox = getSelectedItemFromLeftListBox();
// remove selected item from left list
DefaultListModel leftListModel = (DefaultListModel) leftListBox.getModel();
leftListModel.removeElement(selectedItemFromLeftListBox);
// add removed item from left list to right list
DefaultListModel rightListModel = (DefaultListModel) rightListBox.getModel();
rightListModel.addElement(selectedItemFromLeftListBox);
}
The GUITestHelper provides utility methods to find Swing components contained in a
container. The components can be searched for using some conditions such as their text contents (
JLabel) or field length (JTextField). One could expand this utility to
find other GUI components such as JButtons, JMenuItems, etc. The code for
GUITestHelper is shown below.
/*
* Class with static helper methods for GUI testing.
*
* Author: Santosh Shanbhag
*/
package com.ociweb.testinggui;
import javax.swing.*;
import java.awt.*;
public class GUITestHelper {
/**
* Find out if the container and its sub-containers contain a JLabel with the specifed
* text.
* @param container
* @param labelText
* @return JLabel with the specified text, if found. Otherwise, return null.
*/
public static JLabel findJLabelContainingText(Container container, String labelText) {
Component[] components = container.getComponents();
JLabel labelToFind = null;
for (int i = 0; i < components.length; i++) {
Component component = components[i];
// if the component is a container, find out if it contains the JLabel
if (component instanceof Container) {
// recursion!!!
labelToFind = findJLabelContainingText((Container) component, labelText);
if (labelToFind != null) break;
}
// if the component is a JLabel, find out if it contains the text
if (component instanceof JLabel) {
JLabel label = (JLabel) component;
// compare the label text with the text passed in
if (label.getText().equals(labelText)) {
labelToFind = label;
break;
}
}
}
return labelToFind;
}
/**
* Find out if the container and its sub-containers contain a JTextField of the specified length
* @param container
* @param fieldLength
* @return JTextField having the specified length, if found. Otherwise, return null.
*/
public static JTextField findJTextFieldOfLength(Container container, int fieldLength) {
Component[] components = container.getComponents();
JTextField textFieldToFind = null;
for (int i = 0; i < components.length; i++) {
Component component = components[i];
// if the component is a container, find out if it contains the JTextField
if (component instanceof Container) {
// recursion!!!
textFieldToFind = findJTextFieldOfLength((Container) component, fieldLength);
if (textFieldToFind != null) break;
}
// if the component is a JTextField, find out if it is of the specified length
if (component instanceof JTextField) {
JTextField textField = (JTextField) component;
// compare the length of text field with that passed in
if (textField.getColumns() == fieldLength) {
textFieldToFind = textField;
break;
}
}
}
return textFieldToFind;
}
/**
* Find out if the container and it's sub-containers contain a JButton having the specified text.
*
* @param container
* @param buttonText
* @return JButton containing the text, if found. Otherwise, return null.
*/
public static JButton findButtonWithText(Container container, String buttonText) {
Component[] components = container.getComponents();
JButton buttonToFind = null;
for (int i = 0; i < components.length; i++) {
Component component = components[i];
// if the component is a container, find out if it contains the JButton
if (component instanceof Container) {
buttonToFind = findButtonWithText((Container) component, buttonText);
if (buttonToFind != null) break;
}
// if the component is a JButton, find out if it contains the text
if (component instanceof JButton) {
JButton button = (JButton) component;
// compare the length of text field with that passed in
if ( button.getText().equals(buttonText) ) {
buttonToFind = button;
break;
}
}
}
return buttonToFind;
}
}
Java Swing provides a framework of sophisticated components. Testing Swing applications is challenging but not overly complicated. Indeed there are many gui testing frameworks available that ease the process of GUI testing. This article demonstrated using the test driven approach to developing custom GUI components using Swing and JUnit. A GUI test helper class provides useful methods to find components without the need to expose them using accessors in the original class.
OCI is the leading provider of Object Oriented technology training in the Midwest. More than 3,000 students participated in our training program over the last 12 months. Targeted toward Software Engineers and the development community, our extensive program of over 50 hands-on workshops is delivered to corporations and individuals throughout the U.S. and internationally. OCI's Educational Services include Group Training events and Open Enrollment classes.
For further information regarding OCI's Educational Services programs, please visit our Educational Services section on the web or contact us at training@ociweb.com.
|
|
|