Agile User Interface Development

来源:互联网 发布:淘宝助理描述源码 编辑:程序博客网 时间:2024/04/30 12:45

Agile User Interface Development Agile User Interface Development

by Paul Hamill, author of Unit Test Frameworks
11/17/2004

Overview

"If you're not doing Agile, you're in the past." This is the message of the recent SD Best Practices 2004 conference. Agile processes like XP and Scrum are becoming pervasive in the world of software development. Agile is a sea change, refocusing software developers on quality and speed. Its impact on the practice of software development is already being compared to that of object-oriented design. However, one area of effort has been slow to change: development of the graphical user interface (GUI). Since most software includes some type of GUI, and a good percentage of software development is completely GUI-centric, applying the advantages of Agile to GUI building is of key importance.

What is preventing people from building GUIs in an Agile way? Whether their application is web-based or a desktop application, most developers don't do test-driven development (TDD) of the user interface. This is for a simple reason: unit testing GUI software is hard. Tests that exercise the GUI can be tedious and error-prone, involving complex code to simulate user events, wait while events propagate and controls redraw, and then attempt to check the state as it would appear to the user. Agility depends on doing TDD, but effective tests of specific behaviors are difficult to write for the GUI. The quality and design benefits of Agile have yet to be fully realized on the GUI side of the cube farm.

Agile practices are creeping into this domain. Tools for unit testing GUI elements are proliferating. The JFCUnit framework tests GUIs built using Java Swing. Web-based GUIs can be tested with HTMLUnit, HTTPUnit, jWebUnit, and similar tools. Many GUI builders and toolkits have associated unit testing tools, such as VBUnit for Visual Basic and QtUnit for Qt.

Related Reading

Unit Test Frameworks

Unit Test Frameworks
Tools for High-Quality Software Development
By Paul Hamill

Table of Contents
Index
Sample Chapter

Read Online--Safari Search this book on Safari:
 

Code Fragments only

The tools exist, but the process is still emergent. In TDD, each code change is preceded by a unit test of the new behavior. In GUI development, many changes are just tweaks to the visual appearance, such as changing element positions, text, or color. You might add a button, create a menu item, or construct a dialog. But how and why would you test these kinds of changes? Testing every label or color value would be insane. Likewise, for standard elements like buttons and fields, it's pointless to test their generic behaviors, such as responding to mouse movements, key presses, clicks, and so forth. They are not likely to break. Questions of what to test just increase the innate difficulty of building GUI tests.

The critical question: how do you do test-first GUI development? The answer lies in how the GUI code is structured. Agile gurus such as Kent Beck and David Astels suggest building the GUI by keeping the view objects very thin, and testing the layers "below the surface." This "smart object/thin view" model is analogous to the familiar document-view and client-server paradigms, but applies to the development of individual GUI elements. Separation of the content and presentation improves the design of the code, making it more modular and testable. Each component of the user interface is implemented as a smart object, containing the application behavior that should be tested, but no GUI presentation code. Each smart object has a corresponding thin view class containing only generic GUI behavior. With this design model, GUI building becomes amenable to TDD.

Example: Building a Login Dialog

Let's walk through an example of how to develop a GUI dialog using TDD and the smart object/thin view code design model. First, let's consider the graphic design of the dialog. Agile development calls for minimal up-front design, letting the software architecture evolve through multiple development cycles, but this approach isn't a good idea for GUI design. Designing a user interface is a creative process that should be approached formally, with sketches, prototyping, and usability testing. So, although the code behind the GUI can be designed iteratively using TDD, a sketch of the visual design is a smart first step. The basic design for the login dialog is sketched in Figure 1.


Figure 1. GUI design sketch for login dialog

The dialog is simple, containing user name and password fields, corresponding static text labels, and Login and Cancel buttons. As an initial outline of its behavior, let's decide that a successful login causes the dialog to close, but it remains open in case of login failure. The Cancel button also closes the dialog.

The basic smart object/thin view class design for the code implementing the dialog is shown in Figure 2.


Figure 2. The classes LoginDialog and LoginDialogView

The smart object class LoginDialog will contain a method corresponding to each functional behavior of the dialog. The thin view class LoginDialogView will only contain simple display-related code, and get/set methods to read or set the displayed information. With this approach, only the complex functionality in LoginDialog needs to be unit tested. We can be pretty confident that the simple behavior in LoginDialogView will work.

The first component to build is the smart object LoginDialog. It needs a corresponding test class LoginDialogTest. The first test method will verify the login method, as shown in Figure 3.


Figure 3. The smart object LoginDialog and its test class LoginDialogTest

As the test-first development process dictates, the unit test is written first. The test anticipates and defines the design of the functionality being tested. We need to take a user name and password, and return a login success or failure. A sensible interface to do this is:

boolean login(String username, String password);

The test class LoginDialogTest will test this function. Example 1 shows its initial implementation in the file LoginDialogTest.java.

LoginDialogTest.javaimport junit.framework.*;public class LoginDialogTest extends TestCase {   public void testLogin() {      LoginDialog dialog = new LoginDialog();      assertTrue( dialog.login("user", "passwd") );   }}

This test builds on the JUnit base test class TestCase. The test method testLogin() creates an instance of LoginDialog, calls its login() method, and asserts that the result is true. This code will not compile, since LoginDialog doesn't exist. Following the TDD process, LoginDialog should be stubbed, the code compiled, and the test run to verify that it fails as expected. Then, LoginDialog is given the minimum implementation to pass the unit test, following the Agile mantra of doing "the simplest thing that could possibly work." Example 2 shows the initial version of LoginDialog with the minimum code to pass the unit test, implemented in the file LoginDialog.java.

LoginDialog.javapublic class LoginDialog {   LoginDialog() {}   public boolean login(String username, String password) {       return true;   } }

The code is built using the following commands:

javac -classpath ".;junit.jar" LoginDialogTest.javajavac -classpath "." LoginDialog.java

The classpath must include junit.jar to build the unit test, since it uses JUnit. On Linux, Mac OS X, and other UNIX systems, the classpath should include a colon (:) rather than a semicolon as shown above.

The test is run as follows:

java -classpath ".;junit.jar" junit.textui.TestRunner LoginDialogTest

The unit test passes, hurrah! Unfortunately, the code is bogus. The login() method will always approve the login. No doubt, the customer will not appreciate this level of security. Clearly, the next test to write is one that verifies the login will fail if incorrect credentials are given. Example 3 shows LoginDialogTest with a second test method to fulfill this goal, testLoginFail(). Since both tests use an instance of LoginDialog, the test class is refactored as a test fixture that creates the LoginDialog in its setUp() method.

LoginDialogTest.javaimport junit.framework.*;public class LoginDialogTest extends TestCase {   private LoginDialog dialog;   public void setUp() {      dialog = new LoginDialog();   }   public void testLogin() {      assertTrue( dialog.login("user", "passwd") );   }   public void testLoginFail() {      assertFalse( dialog.login("", "") );   }}

LoginDialog must be made to pass the new test, without failing the first test. The TDD process leads us to build the real functionality we needed, in which the login succeeds if the user name and password are correct, and fails otherwise. Example 4 shows LoginDialog with these changes.

LoginDialog.javapublic class LoginDialog {   private String user = "user";   private String passwd = "passwd";   LoginDialog() {}   public boolean login(String username, String password) {       if (user.equals(username) && passwd.equals(password))         return true;      else          return false;    } }

LoginDialog now passes both tests. To do so, it contains user name and password fields, which must be matched for the login to succeed. Obviously, this is only slightly better than the first version in terms of security. The login code should not contain hard-coded values for authentication! At this point, we could introduce a separate class to contain and authenticate users' login information, which LoginDialog will use. However, this example is about building the GUI, so let's leave the unsafe login code in place and move on.

At this point, we've built the login functionality, and have it covered by unit tests, but have no visible GUI to show for it. What should be done next? With the actual functionality already done and tested, all that has to be done on the GUI side is to create and display the graphical elements, and to call the login() method at the appropriate time. This functionality is generic and can be built simply, so that it doesn't contain complex behavior that could break and would require unit testing. Thus, when building the GUI element, we don't need to do test-first development. Example 5 shows the code for the Swing class LoginDialogView that creates the dialog window, implemented in the file LoginDialogView.java.

LoginDialogView.javaimport java.awt.*;import java.awt.event.*;import javax.swing.*;public class LoginDialogView extends JFrame       implements ActionListener {   protected JTextField usernameField;   protected JTextField passwordField;   protected JButton loginButton;   protected JButton cancelButton;   private LoginDialog dialog;   LoginDialogView(LoginDialog dlg) {      super("Login");      setSize(300, 140);      dialog = dlg;      addControls();      loginButton.addActionListener( this );      cancelButton.addActionListener( this );   }   public void actionPerformed(ActionEvent e) {      String cmd = e.getActionCommand();      if (cmd.equals("Login")          && dialog.login(usernameField.getText(),                                       passwordField.getText())) {         hide();      }   }   private void addControls() {      Container contentPane = this.getContentPane();      contentPane.setLayout(new GridBagLayout());      GridBagConstraints c = new GridBagConstraints();      JLabel label1 = new JLabel("Username:", Label.RIGHT);      c.insets = new Insets(2, 2, 2, 2);      c.gridx = 0;       c.gridy = 0;      contentPane.add(label1, c);      usernameField = new JTextField("", 60);      usernameField.setMinimumSize(new Dimension(180, 30));      c.gridx = 1;      contentPane.add(usernameField, c);      JLabel label2 = new JLabel("Password:", Label.RIGHT);      c.gridx = 0;       c.gridy = 1;      contentPane.add(label2, c);      passwordField = new JTextField("", 60);      passwordField.setMinimumSize(new Dimension(180, 30));      c.gridx = 1;      contentPane.add(passwordField, c);      loginButton = new JButton("Login");      c.gridx = 0;       c.gridy = 2;      contentPane.add(loginButton, c);      cancelButton = new JButton("Cancel");      c.gridx = 1;      contentPane.add(cancelButton, c);   }}

LoginDialogView contains the text field, label, and button elements. Aside from generic GUI behavior, it only has one simple behavior, implemented by the actionPerformed() method. This behavior is that, when the Login button is clicked, the login() method is called. If the login succeeds, the dialog is closed by calling its hide() method.

In order to call the login() function, LoginDialogView needs an instance of LoginDialog, which it receives in its constructor. Otherwise, it consists entirely of GUI-setup and event-handling code. The majority of its code is in addControls(), which simply creates and arranges the GUI elements on the window.

The code for LoginDialogView demonstrates how a GUI thin view element can be designed so that it only contains generic GUI code, and the important application behavior requiring testing resides in a separate, testable smart object. LoginDialogView need only be tested by creating it, looking at it, and making sure it looks and works as expected from the user perspective. Example 6 shows the executable class AppMain that creates the dialog window for hands-on usability testing.

AppMain.javapublic class AppMain {   public static void main(String[] args) {      AppMain app = new AppMain();   }   public AppMain() {      LoginDialog dialog = new LoginDialog();      LoginDialogView view = new LoginDialogView(dialog);      view.show();      while (view.isVisible()) {         try {            Thread.currentThread().sleep(100);         } catch(Exception x) {}      }      System.exit(0);   }}

The class AppMain simply creates a LoginDialog and LoginDialogView, shows the view, sleeps until the view is closed, and then exits.

AppMain is run as shown here:

java –classpath "." AppMain

Running it creates the login dialog window, as shown in Figure 4.


Figure 4. The login dialog window

Interacting with the login dialog verifies that clicking Login with the values shown in Figure 4 causes the login to succeed and the window to close. Trying to log in with other values leaves the window open, since the login has failed. The Cancel button closes the window, as does the window close button. The login dialog works as designed.

Conclusions

We've created a login dialog following TDD and a smart object/thin view design model. The result is well-architected and functional. The functional application behavior is covered by unit tests, and the generic display code doesn't require complex GUI tests. Figure 5 shows the software architecture we've developed.


Figure 5. The classes LoginDialog, LoginDialogView, and LoginDialogTest

At this point, additional features can be added. The login dialog could have a message field to alert the user when the login has failed. Fields for additional login parameters can be added. A separate authentication object can be created and the hard-coded login values removed. Regardless of the changes to be made, TDD and the smart object/thin view model provide a clear direction for their design and implementation. Important application functionality resides in the smart object, where it can be tested, and generic display code resides in the thin view.

For more detailed examples of test-driven GUI development, and extensive coverage of JUnit and other xUnit test frameworks, TDD, and unit test strategies, see my book Unit Test Frameworks, published in November 2004 by O'Reilly Press.

Paul Hamill is a highly experienced software developer with more than ten years of experience developing code using C/C++, Java, and other languages. He is most recently the author of "Unit Test Frameworks."


Return to ONJava.com