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
Tools for High-Quality Software Development
By PaulHamill

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


版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结