摘要 年秋在美国勒海大学亚科卡学院的一份研究报告《世纪美国制造业的战略一个工业主导的观点》中首次提出了敏捷竞争的概念而今天我们似乎已经看到敏捷已经在我们身边形影不离 之前Matrix一则讨论激烈的新闻(l) 也表达了敏捷在今天的热度下面通过一个简单的例子 告诉你如何起步敏捷开发 概述 如果你没有采用敏捷的方式那你就落后了这是最近SD Best Practices 会议上的标语敏捷的方法就象XP和Scrum正在世界的软件发展中变得越来越普遍了敏捷是一个巨大的改变让软件开发者把重心转移到质量和速度上这对已经被比喻成面向对象设计的软件开发有很大的影响但是这种影响的一些方面已经放慢了GUI的开发自从大多数软件包含了一些类型的GUI而且比较多的软件开发的百分比是去完成以GUI为中心的应用敏捷的优势去开发GUI就是关键问题了 什么在阻止人们用敏捷的方法来开发GUI呢?不论他们的应用是基于web的或是桌面应用大多数开发者不做用户界面user interface的测试驱动开发testdriven development (TDD)这都因为一个简单的原因单元测试GUI是很困难的测试GUI是很乏味沉闷而且容易出错的包含了模拟用户事件的复杂代码在事件传播和控制重绘的时候等待然后在他显示给用户之前尝试着检测状态敏捷依赖于测试驱动开发但是为GUI的特定行为来写有效的测试代码是困难的在cube farm(办公农庄用隔断间隔成很多小工作间的办公室? 商务英语)的GUI方面上质量和设计从敏捷方法中受益已经被完全认识到 敏捷实践正在渗透进这个领域单元测试GUI元素的工具激增JFCUnit 框架测试GUI是用java Swing基于Web的GUI能被HTMLUnit HTTPUnit jWebUnit和类似的工具测试许多GUI构造器和工具包和单元测试工具有关系比如VBUnit是为了Visual BasicQtUnit是给Qt用的 工具已经存在了但是处理仍是不确定的在测试驱动开发(TDD)中每个代码的改变都在新行为的单元测试前在开发GUI时许多变化不过是视觉显示上的调整比如改变元素的位置文本或者颜色你可以加一个按钮建一个菜单项或者构造一个对话框但是怎样和为什么你要测试这种变化呢?测试每个标签或者颜色值是很愚蠢的同样的对于标准的元素象按钮和域测试他们通常的行为是没有意义的象对鼠标移动的响应键的按下点击和诸如此类的这些事情是不可能被中断的怎样去测试他们的问题只是徒劳的增添了做GUI测试的难度 一个关键的问题怎样做测试先行的开发?答案就在于GUI的编码是怎样组织的敏捷方式的领袖例如Kent Beck 和 David Astels建议在构造GUI的时候要保持视图对象尽可能是轻量的而且在表面下( below the surface)测试视图层这个 敏捷对象/瘦视图 模型和我们熟悉的 文档视图 及 客户端服务器模式类似但是被应用于个别的GUI元素内容和表现的分离改善了代码的设计使他更模块化和更利于测试每个用户界面的组件被实现为一个敏捷对象包括将要被测试的应用的行为但不包括GUI表现的代码每个敏捷对象有一个相应的瘦视图类只包括普通的GUI行为采用这种设计模式GUI构造变得可以被应用于 测试驱动开发(TDD) 处理了 例子构造一个登录对话框 让我们进入一个例子看看怎样使用TDD和 敏捷对象/瘦视图 代码设计模式去开发一个GUI对话框起初让我们考虑对话框的图形敏捷开发提倡预先最小化设计让软件构架在多次循环开发中重构但是这个方法对GUI设计不是很合适设计一个用户界面是一个创造的过程应该规范地处理画草图做原型和可用性测试然后尽管在GUI下的代码可以用TDD迭代地设计一个形象的设计草图是明智的第一步这个对话框的基本的设计在图中勾画出来 Figure GUI design sketch for login dialog 这个对话框很简单包括用户名和密码域相应的静态文本框和标签登录和取消按钮做为一个他行为的初始轮廓我们决定登录成功的话对话框关闭登录失败的话对话框仍然开着取消按钮也关闭对话框 基本的 敏捷对象/瘦视图 代码类设计的对话框实现在图中表示 Figure The classes LoginDialog and LoginDialogView 敏捷对象类LoginDialog 将包含一个方法对应对话框的每个功能行为瘦视图类LoginDialogView 将只包含简单的和显示相关的代码还有get/set 方法去读取和设置显示的信息在这个过程里只有LoginDialog里复杂的功能需要被单元测试我们可以十分自信在LoginDialogView 里的简单行为可以正常工作 第一个构造的组件是敏捷对象LoginDialog 他需要一个相应的测试类LoginDialogTest 第一个测试方法将要验证登录方法如图所示 Figure The smart object LoginDialog and its test class LoginDialogTest 作为测试先行的开发方法规定首先要写单元测试测试预期和定义了要被测试的功能设计我们需要获得一个用户名和密码然后返回一个登录成功或者登录失败一个用来判断的接口方法来做刚才所述的 boolean login(String username String password); 测试类LoginDialogTest 将测试这个功能例展示了在LoginDialogTestjava 文件中他的初始实现 LoginDialogTestjava import junitframework*; public class LoginDialogTest extends TestCase { public void testLogin() { LoginDialog dialog = new LoginDialog(); assertTrue( dialoglogin(user passwd) ); }} 这个测试是基于JUnit基础测试类TestCase的测试方法testLogin()创建了一个LoginDialog 的实例调用了他的login()方法然后判定结果是真这段代码将不会编译因为LoginDialog 不存在在TDD过程后LoginDialog 将生成和保存代码编译后测试运行验证将象预期的那样失败(因为方法没有实现)然后 LoginDialog 为了通过单元测试给出最小的实现遵照敏捷的圣条 做 可能工作的最简单的事情(the simplest thing that could possibly work)例展示了最初的LoginDialog 版本用最少的代码通过了单元测试实现在LoginDialogjava 文件里 LoginDialogjava public class LoginDialog { LoginDialog() {} public boolean login(String username String password) { return true; } } 使用下面的命令来运行代码 javac classpath ;junitjar LoginDialogTestjava javac classpath LoginDialogjava classpath 必须包括junitjar 来运行单元测试因为他使用了JUnit在LinuxMac OSX还有其他的UNIX系统上classpath将包含一个冒号(:)而不是想下面那样用一个分号 测试将如下运行 java classpath ;junitjar junittextuiTestRunner LoginDialogTest 单元测试通过了真好!不幸的是这个编码只是模拟一下Login()方法将总是批准登录毋庸置疑客户将不会欣赏这种水平的安全机制显然要写的下一个测试是验证如果给的条件不正确的话将失败例展示了LoginDialogTest 的第二个测试方法去实现这个目的testLoginFail() 既然两个测试都使用一个LoginDialog 的实例测试类被重构为在他的setUp() 方法里创建一个固定的测试用的LoginDialog LoginDialogTestjava import junitframework*; public class LoginDialogTest extends TestCase { private LoginDialog dialog; public void setUp() { dialog = new LoginDialog(); } public void testLogin() { assertTrue( dialoglogin(user passwd) ); } public void testLoginFail() { assertFalse( dialoglogin( ) ); }} LoginDialog必须得通过新的测试不能在第一次测试的时候有失败TDD过程引导我们构造我们需要的真正的功能在用正确的用户名和密码登录的时候能成功登录如果不是就失败例展示了按此修改的LoginDialog LoginDialogjava public class LoginDialog { private String user = user; private String passwd = passwd; LoginDialog() {} public boolean login(String username String password) { if (userequals(username) && passwdequals(password)) return true; else return false; } } LoginDialog现在能通过所有的测试为此他包括了符合成功登录条件的用户名和密码域显然这只是比第一个版本的安全性能稍微好一些登录代码不应该包含认证的硬编码!基于这点我们应该引入一个单独的类来包含LoginDialog 用的验证用户的登录信息然而这个例子是关于GUI构造的那让我们暂停这个不安全的登录代码继续GUI方面 现在我们已经建立了登录功能并用单元测试覆盖了他但没有可视的GUI来显示它那下一步该做什么呢?对于已经作的和测试的实际功能在GUI方面做的是创建和显示图像元素然后在适当的时候调用login()方法这个功能是普通和容易建立的所以他不包含能中断和需要单元测试的复杂行为因此当建立GUI元素时我们不需要去做测试先行的开发例展示了创建对话框窗口的Swing类LoginDialogView 他的实现在LoginDialogViewjava文件 LoginDialogViewjava import javaawt*; import javaawtevent*; import javaxswing*; 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( ); dialog = dlg; addControls(); loginButtonaddActionListener( this ); cancelButtonaddActionListener( this ); } public void actionPerformed(ActionEvent e) { String cmd = egetActionCommand(); if (cmdequals(Login) && dialoglogin(usernameFieldgetText() passwordFieldgetText())) { hide(); } } private void addControls() { Container contentPane = thisgetContentPane(); contentPanesetLayout(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); JLabel label = new JLabel(Username: LabelRIGHT); cinsets = new Insets( ); cgridx = ; cgridy = ; contentPaneadd(label c); usernameField = new JTextField( ); usernameFieldsetMinimumSize(new Dimension( )); cgridx = ; contentPaneadd(usernameField c); JLabel label = new JLabel(Password: LabelRIGHT); cgridx = ; cgridy = ; contentPaneadd(label c); passwordField = new JTextField( ); passwordFieldsetMinimumSize(new Dimension( )); cgridx = ; contentPaneadd(passwordField c); loginButton = new JButton(Login); cgridx = ; cgridy = ; contentPaneadd(loginButton c); cancelButton = new JButton(Cancel); cgridx = ; contentPaneadd(cancelButton c); }} LoginDialogView包含了文本域标签和按钮元素除了普通的GUI行为外他只是有一个简单的行为被actionPerformed() 方法实现这个行为就是当登录按钮被点击后login()方法被调用如果登录成功对话框就被所调用的hide()方法所关闭 为了调用login()函数在LoginDialogView 构造器里需要接收一个LoginDialog实例另外他组装了完整的GUI设置和事件处理代码大部分代码在addControls() 里他简单的创建和排版了窗体上的GUI元素 LoginDialogView 代码示范了一个GUI瘦视图元素怎样被设计使它只包含普通的GUI代码而把重要的需要测试应用的行为放到一个单独可测试的敏捷对象中 LoginDialogView 只需要通过创建它来测试察看他从用户的角度确认它看起来和运行起来象期望的那样例展示了可执行的类APPMain它创建了对话窗体来传递可用性测试(指的是传递loginDialog的实例) AppMainjava public class AppMain { public static void main(String[] args) { AppMain app = new AppMain(); } public AppMain() { LoginDialog dialog = new LoginDialog(); LoginDialogView view = new LoginDialogView(dialog); viewshow(); while (viewisVisible()) { try { ThreadcurrentThread()sleep(); } catch(Exception x) {} } Systemexit(); }} AppMain 类简单的创建一个LoginDialog 和LoginDialogView 显示视图休眠直到视图关闭然后退出 AppMain 象下面一样运行 java –classpath AppMain 运行它创建登录对话框如图所示 Figure The login dialog window 和登录对话框交互验证了用图所示的值登录会登录成功然后窗体关闭试着用其它的值登录窗体将保持打开因为登录失败了取消按钮关闭窗体就象窗体的关闭按钮一样这个登录对话框就如同设计的那样运行 解决方案 我们已经根据TDD创建了登录对话框和一个敏捷对象/瘦视图设计模式得到了一个有很好构架和功能的程序有功能的应用行为被单元测试所覆盖普通的用来显示的代码不需要复杂的GUI测试图展示了我们所开发的这个软件的构架 Figure The classes LoginDialog LoginDialogView and LoginDialogTest 基于此其他的特性可以被加入登录对话框可以有一个消息域去提醒用户登录失败其他的登陆参数域也可以被加入一个单独的验证对象可以被创建硬编码的登录值可以被删掉不管怎么变化TDD和敏捷对象/瘦视图模式提供了一个设计和实现上的清晰的方向重要的应用功能是在于可以测试的敏捷的对象和在瘦视图中普通的显示用的代码的 |