Agile User Interface Development
by Paul Hamill,author of Unit Test Frameworks11/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 |
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.java import 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.java public 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.java javac -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.java import 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.java public 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.java import 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); 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.java public 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
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 11 of 11.
- That sound like...
2004-11-19 13:29:36 JumpinJackFlash [Reply | View]
Reply | View]
This is just Martin Fowler's application facade pattern. For a more detail text,see is 27 pages article on is site at http://www.martinfowler.com/apsupp/appfacades.pdf
One thing about this pattern,is that it won't cut all unit test to the form,but it would more easily decouple those between the functionality of the form and those specific to the user interaction with the form itself.
- Do you get Agile?
2004-11-18 17:16:23 DoubleSkulls [Reply | View]
Reply | View]
I'm sorry but you obvIoUsly don't understand Agile iterative development at all if you believe that GUIs are somehow special and require upfront design that other parts of a system don't.
In fact the opposite is probably true. The UI is the area customers generally want the most change (moving buttons,changing text/colour) so its the most appropriate for minimal upfront design and multiple iterations.
- smart object/thin view model
2004-11-18 09:00:43 PaulHamill [Reply | View]
Reply | View]
This article suggests using a smart object/thin view model for development of GUI objects. It is consistent with MVC,because there is a model and a view,but has an important difference: the view does not contain any business logic or complex functionality that requires unit testing. This way,you only need to do TDD on the model. There is no need to jump through extra hoops (or use GUI testing tools) to test the generic GUI code in the view class.
For a similar perspective,see Michael Feather's well-known article
The Humble Dialog Box.
- smart object/thin view model
2004-11-19 04:35:32 AndyP [Reply | View]
Reply | View]
How do you test that you view code contains no business logic? Surely TDD says you can't trust any code you aren't testing?- smart object/thin view model
2004-11-29 06:52:41 ipreuss [Reply | View]
Reply | View]
You can't test that "view code contains no business logic". But you certainly can be quite sure that it doesn't contain business logic without having to write tests.And no,TDD doesn't say "you can't trust any code your aren't testing". Actually it explicitely says to not test code that "cannot possibly break",i.e. simple getters or setters.
Last but not least,TDD is a tool,not a dogma.
- smart object/thin view model
- smart object/thin view model
- I'm sorry....
2004-11-18 02:11:31 AndyP [@L_403_46@ | View]
@L_403_46@ | View]
In this article you suggest putting functionality into a separate class (lets call this class a 'model'),and having all the display code in another class (the 'view'). You then apply TDD to the model.
Maybe I don't get this but surely suggesting the use of MVC is blindingly obvIoUs. I also think that only testing the model,defeats the entire purpose of testing a GUI. It's realtively easy to test the model in isolation. How does your glorIoUs TDD try to test for all the strange and unpredictable ways that users click on buttons in a GUI? GUI's are event driven,and typically multi-threaded. And all this in the view code which you suggest we shouldn't test.
Please help me to understand what you are trying to say becuase from my naive viewpoint it just seems to be,'use MVC patterns,and only test the model'.
- I'm sorry....
2004-11-18 06:47:17 b.j. [Reply | View]
Reply | View]
I agree with AndyP entirely.
A better example may be to find a test tool that can test gui interactions. HTTP::Recorder and PAMIE are examples of such tools for testing web applications.- I'm sorry....
2004-11-26 11:00:35 decoder [Reply | View]
Reply | View]
Gotta disagree here fellas. I totally agree w/your critique; this article has no useful new information and is incredibly naive. And I totally agree that there are things in the view that absolutely must be tested for. What I disagree about is that you have to go straight to a scripted integration testing tool from this decidedly simplistic 'test the model' approach. In fact,the Gamma/Beck book Contributing to Eclipse has a lot of excellent examples of writing UI unit tests. Unfortunately,that is using a framework that is not really hosted like web frameworks are. Even the new Manning book on JUnit Recipes punts on this one. Someone needs to do much more work on this and I tend to blame the framework developers. Why doesn't JSP and JSF come with a simple way to do unit tests? All they would need to do is make it very simple to compile a page (without having to run the whole container) and/or mock it completely.- I'm sorry....
2004-12-03 19:29:29 jbrains [Reply | View]
Reply | View]
I don't understand how JUnit Recipes "punts" on this issue. It includes a collection of recipes related to testing web user interfaces /in isolation/ -- something that a majority of even the TDD community claims is not worth the effort. I have test-driven web UIs implemented with Velocity with relative ease and to stunning effect,and included that experience in the book. What more are you expecting?
- I'm sorry....
2004-12-06 09:05:42 decoder [Reply | View]
Reply | View]
Actually,I didn't explain that point well enough. If you read that whole section of the recipes book,it starts by saying that most people think that most people think it's not worth the effort,but then,ultimately,because Jasper won't compile pages for them,they just walk away. The very next section is about Velocity and how simple it is to test templates there.
Furthermore,their whole 'gold master' approach is super simplistic and not given much serIoUs treatment.
Let me be super-specific. In JSF,the framework kicks back messages automatically if a validation or conversion fails. I made my own custom component to pick up those messages and turn the style of the corresponding field's label to red text. Wouldn't it be great to have a unit test that creates a form,forces an error and then checks to see that the style of the label in the response (the same page being redrawn) was red?
Most people think of unit tests as being simply so we can figure out if something works. Automation yields a much broader value: being able to know that something works at all times.
- I'm sorry....
- Testing JSPs etc.
2004-11-29 06:55:26 ipreuss [Reply | View]
Reply | View]You can compile a page using the ServletRunner of ServletUnit (which is part of HttpUnit),a very lightweight servlet container for testing purposes (it doesn't even implement the http protocol).
- I'm sorry....
- I'm sorry....
- I'm sorry....