Skip to main content

Developing Swing Components Using Simulators

June 23, 2004

{cs.r.title}








Contents
What are Simulators?
Getting Started
Dynamic Simulators and Controls
Scenarios
Wrap-Up
Resources

User interfaces often end up a series of tightly integrated components that are hard to build, maintain, and test. But it doesn't have to be that way! In this article, you'll learn about a technique for developing encapsulated components. You'll also learn about how to test graphical parts of your application that JUnit can't, such as layouts and other visual properties.

What are Simulators?

If you ever find yourself starting your full application just to test a component that's part of it, you're wasting precious development time. You have to wait for the application to initialize each time you make a change. Even though it may seem like a small amount of time, this can really add up. There can also be development problems trying to integrate a component before it's ready to go live.

There are a number of reasons we might do this. One reason is that unit tests can help test the functionality of a component, but you need to start the application to test its appearance. Another reason may be that you are using a GUI builder and need to test the component with real data. Yet another reason might be that you aren't using a GUI builder at all (I don't) and need to test layouts and other visual settings during development.

We can solve these problems using a development technique I call simulators. Simulators are special classes for graphically testing, or simulating, your component in various conditions. Using simulators, you can run your component in isolation for development, so you don't have to start the entire application. Additionally, we will look into dynamic data and scenarios where you can plug realistic data into your component to see how it will react in the main application. Using simulators, you will be able to develop the majority of your component without having to start the complete application -- saving time and effort during development.

Getting Started

Let's start with an example: developing a contact information display. So far, the requirements are:

  • Display name, home phone number, work phone number, and email address.
  • Prepend "H:" for home phone number and "W:" for work phone number to make a graphical distinction in the display.

The end result looks like Figure 1.

Figure 1
Figure 1. Simple contact window

Just like when we write unit tests, we are not concerned with the actual implementation of the class. We are concerned with the interface (see the Resources section below to download the complete source code).

public interface ContactInfoPanel {
    void setName(String firstName);
    void setHomePhone(String homePhone);
    void setWorkPhone(String workPhone);
    void setEmail(String email);
}

We can easily write a unit test to make sure that the "H:" and "W:" are prepended to their respective phone numbers. What we can't test is how the component looks: where the components are showing on the screen, how the component reacts when it's expanded or contracted, and so on.

Let's create a simulator to graphically test the component. We'll call it ContactInfoGraphicalDisplaySimulator. In its basic form, a simulator is simply a class with a main method to display a component (don't worry, we'll get more complicated later). At this stage, all the main method needs to do is configure and display the contact info panel in a frame. Here is the code:

public class ContactInfoGraphicalDisplaySimulator {

    public static void main(String[] args) {

        //create the frame
        JFrame frame = new JFrame();
        frame.setTitle("ContactInfo Display");
        frame.setBounds(400,400, 250,120);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        //create the ContactInfoGraphicalDisplay
        ContactInfoGraphicalDisplay display =
            new ContactInfoGraphicalDisplay();
        //layout frame
        Container contentPane = frame.getContentPane();
        contentPane.add(display, BorderLayout.CENTER);
        contentPane.add(new JPanel(), BorderLayout.NORTH);
        contentPane.add(new JPanel(), BorderLayout.SOUTH);
        contentPane.add(new JPanel(), BorderLayout.EAST);
        contentPane.add(new JPanel(), BorderLayout.WEST);;

        //configure the contact info display
        display.setName("Jonathan Simon");
        display.setHomePhone("123.456.7890");
        display.setWorkPhone("111.222.3333");
        display.setEmail("jonathan@simon.com");

        //show the frame
        frame.show();
    }
}

I have seen several developers add main methods to components they are developing. The use of simulators has several benefits over this approach. If we add a main method to the ContactInfo display itself, we have to decide what to do when we are done with development. We could delete it, but we may be forced to rewrite it in the future when we have to do more development or maintenance on the component. On the other hand, leaving the main method would allow additional launch points to be released into production. With the simulator approach, the test code lives in a completely separate class. Once it's a class, it's a full-fledged citizen that can be added to source control and maintained. We can easily delete all of the simulators during the build process so that they never get released into production. And we will still have the simulator around next time we need to do any maintenance or new development.

Dynamic Simulators and Controls

At this point, we have a functional simulator, but it's not very smart -- it just
shows the component with a static set of data. The power of simulators really
comes into play when we have dynamic content. Let's assume we have
a new requirement: if either the phone number or email address is blank, it
should be removed from the panel, so we don't end up with a panel with a space
as in Figure 2.

Figure 2
Figure 2. The contact info display with improper spacing

Instead, we are required to produce an evenly spaced panel like Figure 3.

Figure 3
Figure 3. The same contact info display, with a modified layout to remove the space

We could keep restarting the simulator and change the display settings each time, but that isn't very effective. Plus, the simulator is then of little use outside of a development environment, because it actually requires re-coding and compiling to change the settings.

We can solve this by creating a separate frame to control the contact info panel. I usually start off by creating one static method to create the control frame and one to create the display name. We can refactor our current simulator's main method and move the code to a new method called createDisplayFrame.

private static JFrame createDisplayFrame(ContactInfoGraphicalDisplay display){
    JFrame frame = new JFrame();
frame.setTitle("ContactInfo Display");
    frame.setBounds(400,400, 250,120);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    frame.getContentPane().setLayout(new BorderLayout());

    Container contentPane = frame.getContentPane();
    contentPane.add(display, BorderLayout.CENTER);
    contentPane.add(new JPanel(), BorderLayout.NORTH);
    contentPane.add(new JPanel(), BorderLayout.SOUTH);
    contentPane.add(new JPanel(), BorderLayout.EAST);
    contentPane.add(new JPanel(), BorderLayout.WEST);

    return frame;
}

The next step is to write the createControlFrame method. We want to create a separate frame with a text field for each of the display fields in the contact info panel; name, home phone, work phone, and email address. This way, we can change the display settings on the ContactInfo display at run time rather than compile time. Additionally, we can watch the layout change in real time as the display information changes. Figure 4 shows the control frame for the contact info display.

Figure 4
Figure 4. The contact info control frame

We'll add DocumentListeners so that editing any of the text fields immediately changes the ContactInfoGraphicalDisplay in our display frame. This display frame in Figure 5 shows the home number changing along with the control panel from Figure 4.

Figure 5
Figure 5. The contact info display, mid-edit

The control frame needs a text field for each configurable parameter of the contact info display. In our case, we have the four parameters: the two phone numbers, the name, and the email address. So we need to create textFields for each one.

JTextField nameTextField = new JTextField();
JTextField homePhoneTextField = new JTextField();
JTextField workPhoneTextField = new JTextField();
JTextField emailTextField = new JTextField();

We are only using text fields to edit our properties, since they are all string- and number-based. If we had a special type, such as a color, we would use a specialized color-selection component.

Now we can add a listener to each text field, which will modify the contact info display as the control frame is edited. A DocumentListener works really well for this purpose, since it doesn't require a "set" or "apply" button -- the display simply reflects the change as the control frame is updated.

nameTextField.getDocument().addDocumentListener(
    new DocumentListener() {
        public void changedUpdate(DocumentEvent e) {
            display.setName(nameTextField.getText());
        }

        public void insertUpdate(DocumentEvent e) {
            display.setName(nameTextField.getText());
        }

        public void removeUpdate(DocumentEvent e) {
            display.setName(nameTextField.getText());
        }
    }
);

homePhoneTextField.getDocument().addDocumentListener( ... );
workPhoneTextField.getDocument().addDocumentListener( ... );
emailTextField.getDocument().addDocumentListener( ... );

Scenarios

The last modification we'll make to this simulator is introducing the idea of scenarios. We are going to enter the same information into the text fields over and over again, each time we modify the component, to make sure that the layouts are still functioning correctly. This is clearly inefficient, since we are usually checking for specific cases. An example: what happens if no phone numbers are present and we should only display the name and email address? Rather than manually enter this information every time, we can store scenarios so that we can repeat them efficiently.

Let's start with a data object encapsulating a display setting and a display name.

private static class ContactInfoScenario {
    private String displayName, name, email, workPhone, homePhone;

    public ContactInfoScenario(String displayName,
        String name,
        String email,
        String workPhone,
        String homePhone) {
        this.displayName = displayName;
        this.name = name;
        this.email = email;
        this.workPhone = workPhone;
        this.homePhone = homePhone;
    }

    public String getDisplayName(){ return displayName; }
    public String getName() { return name; }
    public String getEmail() { return email; }
    public String getWorkPhone() { return workPhone; }
    public String getHomePhone() { return homePhone; }
    public String toString() { return getDisplayName(); }
   
}

Next, we can create a static method to create our stock scenarios.

private static Vector getScenarios(){
    Vector list = new Vector();
    list.add(new ContactInfoScenario(
        "Normal", //scenario display name
        "Jonathan Simon", //name
        "jonathan@simon.com", //email
        "123.456.7890", //home phone
        "111.222.3333")); //work phone
    list.add(new ContactInfoScenario(
        "No email", //scenario display name
        "Jonathan Simon", //name
        "", //email
        "123.456.7890", //home phone
        "111.222.3333")); //work phone
    list.add(new ContactInfoScenario(
        "No phones", //scenario display name
        "Jonathan Simon", //name
        "jonathan@simon.com", //email
        "", //home phone
        "")); //work phone
    list.add(new ContactInfoScenario(
        "Name only", //scenario display name
        "Jonathan Simon", //name
        "", //email
        "", //home phone
        "")); //work phone
    list.add(new ContactInfoScenario(
        "Long email", //scenario display name
        "Jonathan Simon", //name
        "jonathan_scott_simon_really_long@simon.com", //email
        "123.456.7890", //home phone
        "111.222.3333")); //work phone
    return list;
}

The last step is to add a combo box with these scenarios.

final JComboBox comboBox = new JComboBox(getScenarios());
comboBox.addItemListener(
    new ItemListener() {
        public void itemStateChanged(ItemEvent e) {
            ContactInfoScenario scenario =
                (ContactInfoScenario) comboBox.getSelectedItem();
            nameTextField.setText(scenario.getName());
            workPhoneTextField.setText(scenario.getWorkPhone());
            homePhoneTextField.setText(scenario.getHomePhone());
            emailTextField.setText(scenario.getEmail());
        }
    }
);

Figures 6 and 7 show the final scenario-based simulator and display frame. Specifically, they display the "long email" scenario, where we have an intentionally long email address that we expect to extend beyond the edge of the panel. Figure 6 shows the scenario control frame. Notice the JComboBox at the bottom of the frame for selecting a scenario. Figure 7 shows the resulting contact info display.

Figure 6
Figure 6. The scenario control frame

Figure 7
Figure 7. The contact info display running the "long email" scenario

Notice the text fields are still editable in the control frame. This way, we have our set of scenarios, but we can always quickly try something new without having to rebuild the simulator.

Scenarios end up working like graphical test cases. For any specific dynamic situation, we write a new scenario. We can run JUnit tests to test that the logic is working correctly. And when we're done with that, we can run our graphic simulator tests to make sure the display graphically and correctly readjusts given the new settings.

When modifying an existing component, I typically run the simulator and scroll through all of the scenarios to visually make sure the display is functioning correctly. This is just like running a visual test suite. And as with test cases, when I find a bug with the display, I write a scenario to cause the particular issue and run it and all of the other scenarios each time the display changes.

Wrap-Up

Simulators have really helped me develop encapsulated components for my clients. It makes it much easier to develop when I am focusing on a single component. And it's also much faster to bring up a simulator when I want to make a change to a component then to worry about launching the full client with the necessary data set. The list goes on.

Now that we have a development strategy for components, we can start to look at the broader application. Next time, we'll take a look developing a stub server: a locally running server for client development and testing. Using the stubs, you can develop and test your entire application without relying on an server running -- or even being written yet.

A big thanks to all the guys at Liquidnet for introducing me to the idea of simulators.

Resources

Jonathan Simon is a developer and author specializing in user interaction.
Related Topics >> GUI   |   Swing   |   Testing   |