ChinaUnix.net
 >> ChinaUnix.net > Java

诊断 Java 代码: 设计“可测试的”应用程序

作者:cinc     发表时间:2002/10/08 10:05am

http://www-900.ibm.com/developerWorks/cn/java/j-diag/part15/index.shtml

心存测试概念进行代码设计时的七条基本原则
Eric E. Allen (eallen@cs.rice.edu)
博士研究生候选人,Rice 大学
2001 年 9 月

在诊断 Java 代码的这一部分中,Eric Allen 暂停了对具体错误模式的讨论,转而选择讨论关于设计易于、甚至我们乐于测试的软件的问题。他概述了七条设计原则,这些原则能大幅提高您编写测试代码的效率并因此提高结果代码库的健壮性。请在讨论论坛与作者和其它读者共享您关于本文的心得。
谨以本文献给周二攻击的受害者,向灾难挑战的英雄们和美国人民的钢铁意志。
当设计大型程序的时候,您必须时刻留心不同设计选项对诸如性能和可扩展性这样的特征的影响。随着软件产品的日渐复杂及其无所不在的部署,软件的“可测试性”也成了更重要的考虑事项。

彻底测试代码的重要性是显然的。花在编写测试和测试代码上的时间和精力给您带来的回报是维护成本的大幅降低。

然而,除非您很小心,否则您花在测试代码上的精力可能会首先达到花在编写代码上的精力的几倍!我曾看到程序员们齐心协力地对他们的全部代码进行单元测试,结果花在上面的时间使大多数人都以沮丧而告终。

幸运的是,没有必要这样。在您设计软件的时候应用一些基本原则,编写易于测试、甚至使测试成为乐趣的代码是可能的。

跟其它编码原则一样,这些原则也不是不容置疑或不可改变的教条。有时候打破这些规则也是必要的。因此,理解每条原则背后的动机和判断何时这些动机不适用(或应让位给更关心的问题)的能力是很重要的。

原则 1. 到 GUI 视图的外面去
尽可能把代码移到 GUI 视图的外面。然后各种 GUI 动作就能成了模型上的简单方法调用。为什么您需要这样做呢?

对 GUI 测试者来说,通过方法调用测试功能比间接地测试功能容易的多。
另一个好处是它使修改程序功能而不影响视图变的更容易。
当然,视图中也可能存在错误。在理想情况下,对程序的测试将同时检查模型和视图。(想更多了解测试视图,请参阅我关于 Liar View 错误模式的文章或 Jeffries 等人的 Extreme Programming Installed。这两个链接都在参考资料部分。)

原则 2. 使用类型进行错误检查
类型是您的朋友 — 尽可能多地用类型系统自动检查错误。

类型能在程序运行之前自动捕捉程序中的错误。没有静态类型检查的话,类型错误将作为破坏者逗留在您的程序中,直到恰当的执行路径碰巧把它揭露出来为止。

最大限度地发挥使用类型的长处是棘手的。通常,一组数据结构可以在一个抽象级别上一起使用,或者被分出,成为一个单一的、更高抽象级别的一个新的相关数据类型。

事实上,编程语言自身的历史可以看成是可以编程的抽象级别的逐渐提高。汇编语言提供了比特到整数和浮点数的抽象。接下来是记录和函数抽象,然后又是诸如对象、类、线程以及异常这样的抽象。

在每一抽象级别上,达到与更高级别抽象一致的功能是可能的,但那实质上仅仅是耗费更多精力,冒更多的错误风险。

在面向对象语言(其它现代语言也一样)中,一个程序员在设计抽象上有很大的灵活性。在哪个抽象级别上设计程序就成了基于折衷的决定,比如由抽象级别提供的更多的健壮性和由于不能在更低抽象级别上工作而带来的表达性(有时是性能)的损失。

通常,高级别抽象带来的健壮性和简单性的价值很少被其它考虑事项超过。(要了解对这个问题的更多讨论,请参阅我关于 Impostor Type 错误模式的文章,在参考资料部分有它的链接。)

原则 3. 使用调节器避免“故障线路”(fault line)
我用“故障线路”来指独立组件之间的接口,独立组件之间和组件与其相应子组件之间相比,很少有交互。这种故障线路的一个典型示例是 GUI 视图和它的模型之间的接口。其它示例包括在编译器中处理的不同阶段之间的接口或操作系统的内核和用户界面之间的接口。

找出程序的故障线路,然后用具有转发功能的调节器快速访问聚合组件。

沿着故障线路隔离测试每个组件通常更容易。但如果每个组件暴露的对象有很多,或者组件中您想测试的一些对象只有通过多个嵌套引用才能访问,那么测试就会变的很乏味。

不用隔离测试,而是拥有您在它上面调用您想测试的各种方法的单个调节器对象通常是有帮助的。这个对象然后能把这些方法调用转发到适当的地方。

沿着相同线路,设计和自己的测试代码串联在一起的程序组件接口是有益的。这将使您把注意力集中在使这些接口尽可能简单上。

原则 4. 方法:小型签名和缺省参数
使用小型方法说明和重载带缺省方法参数的方法将使您在测试中调用这些方法变的愉快的多。否则,在测试这些方法时您将不得不构造额外参数。如果参数很大,那么将很快导致代码膨胀。更糟的是,它会诱使您编写比在其它情况下更少的测试。

原则 5. 访问器不应修改内存状态
请在您的测试中使用不修改内存状态的访问器来检查对象状态。

在某些方面,测试和实验室试验相似。它们都想证明特定假设有效。如果特定检查动作改变了该领域的状态,那么要这样做会变得困难的多。

与量子力学领域不同,计算机进程的状态可以不修改就被检查。使用这种原则对您有好处。

原则 6. 用接口说明外部程序组件
用接口说明外部程序组件使得我们可以容易地在测试案例中模拟这些组件。

这条原则能节省大量时间,特别是当外部组件的实现还未完成时。通常,大多数基本组件都不能准时可用。如果这些组件不在适当位置您就不能测试您自己的代码的话,那么您就在朝灾难走去。您的客户不会关心您只有两个小时来集成迟到了两周的组件。他们知道的全部就是整套产品被延期了和这是违约的。

原则 7. 优先编写测试代码
优先编写测试代码。这是标准的 XP 方法,但却总有一种忽视它的诱惑。

每次我屈服于这种诱惑时,我都感到后悔。假设您正努力生产正确的代码,那么您 好象能从推迟编写测试代码中节省的时间其实只是一个幻想。

注意:这不是说您应该一次性编写全部测试代码后,再一次性全部实现。编写一些测试代码,实现它们,再编写一些测试代码,再实现它们等等是个更好的办法。设计以这种方式得以进展;在实现阶段捕捉错误并在下一组测试中改正它。以这种方式编写测试也更少会使人畏缩。

代码比您需要的还多?
只需一点点努力,就可能容易地对任何程序进行彻底的测试。当然,不可避免存在这些原则不适用的情况;于是,看起来好像不可能对功能进行测试。

当出现这些情况时,我尽力退一步地看这个问题,“我怎样才可能测试这种代码?”相反地,我问自己,“我怎样才能以可测试方式编写这些代码呢?”这种想法上的改变的结果经常是增加了大量 仅仅服务于简化测试的功能。

什么?别担心;出现这种情况完全正常。

就象很多现有的设计模式,它们只是为了增加程序的可扩展性就往程序中添加很多类(例如 visitor、decorator 等等),开发简化测试的新模式是可以接受的。实际上,面向对象语言的很多特征都是为了简化扩展而包含进去的;为什么语言的未来版本(或全新的语言)不应包含简化测试的特征。

对 Java 语言来说,这已经开始。人们计划在未来版本中包含很多更强大的类型系统、断言(assertion)等等。就象面向对象的语言已经增加了我们重用和扩展现有代码的程度,将来,面向测试的设计和特征将帮助我们增强新老代码的健壮性。

参考资料

参加本文的讨论论坛。


DePaul 大学的软件工程系在检测 Java 空指针异常的自动化法则方面做了一些工作。


JUnit 主页提供讨论程序测试方法的很多有趣文章的链接,并提供 JUnit 的最新版本。


如果您正用 VisualAge for Java 开发代码,请看看这篇文章 VisualAge for Java 3.5 中的调试和单元测试。


查阅 iContract ,一个方便您往 Java 代码中添加断言的工具。


Roy Miller 和 Chris Collins 提供的文章“XP distilled”(developerWorks,2001 年 3 月),说明了极端编程方法如何为您的 Java 工程带来更大的成功。


查阅 ExtremeProgramming.org 上的官方的 XP 编程规则。


想了解如何测试视图的一些观点,看看 Eric Allen 关于 Liar View 错误模式的文章(developerWorks,2001 年 4 月)。


想更多了解测试视图的观点,Jeffries、Anderson 和 Hendrickson 的 Extreme Programming Installed(Addison-Wesley,2001 年)会很有用。


想了解为什么使用更高级别的抽象是更有益的(即使它只是在逐步走向前台),请参阅 Eric Allen 关于 Impostor Type 错误模式的文章(developerWorks,2001 年 7 月)。


在 developerWorks Java 技术专区上查找更多的 Java 参考资料。


关于作者
Eric Allen 从 Cornell 大学获得计算机科学及数学的学士学位,并且是 Rice 大学 Java 编程语言小组的博士候选人。在回 Rice 完成学位前,Eric 是 Cycorp,Inc. 的 Java 开发者带头人。他还在 Javaworld 主持“Java 初学者”讨论论坛。他的研究包括在源程序和字节码级别上 Java 语言的语义模型和静态分析工具开发。Eric 还帮助开发 Rice 的 NextGen 编程语言编译器,NextGen 是一个支持泛运行时类型的 Java 扩展。可通过 eallen@cs.rice.edu 与 Eric 联系。
 


此文章相关评论:
该文章有4个相关评论如下:(点这儿可以发表评论)
cinc 发表于: 2002/10/08 10:36am
如何生产出高质量的软件,很大程度上取决于代码的质量。
XP提出:高质量的代码,应该是可测试的代码。所以他提倡 test first design。

XP 推荐的test first design 的开发过程:
比如说你要写一个类,按以下步骤来:

1.写这个类的框架,写的代码只要能够进行测试就可以了。
2.写测试代码,完成测试框架,编译源程序和测试程序,看到测试运行失败。
3.写程序,然后运行测试,直到所有测试通过。
4.以后修改代码后,运行测试,如果所有测试通过,修改代码成功。

举例说明:如果你写一个更新数据库的方法 updateThread:

Thread 是论坛里的一篇文章 属性有 title(标题), content(内容), notify(是否通知作者)

1.写这个类的框架:
class Thread { //Thread Bean
   ...  // 一堆的 get,set 方法
}
class ThreadDAO {
   /**
    * update Thread from DB
    */
   public void updateThread( Thread thread )
           throws SQLException, ThreadNotFoundException{
       // 里面不写任何代码,等到写完测试后再在此写代码
       // 现在写的 ThreadDAO 的代码只是为了编译通过。
   }
}

2.写测试代码,使用 JUnit 作为测试框架
 我写了三个测试函数:
 测试 NULL Object 的情况:testUpdateThreadNULLObject()
 测试 Thread Not Found  :testUpdateThreadNotFound()
 测试 正常的 更新       :testUpdateThreadNormal()
 
public class ThreadDAOTest extends TestCase {

...

public void testUpdateThreadNULLObject() {
   try{
       final ThreadDAO dao = new ThreadDAO();
       final Thread pt = null;
       dao.updateThread(pt);
       fail ("Except NULL Object");
   }catch( final SQLException sqlException ){
       fail("Unexpected excetpion: " + sqlException );
   }catch( final ThreadNotFoundException threadNotFoundException ){
       fail("Unexpected excetpion: " + threadNotFoundException );
   }catch ( final IllegalArgumentException IllegalArgumentException ){
       //pass
   }
}


public void testUpdateThreadNotFound() throws Exception {
   try{
       final ThreadDAO dao = new ThreadDAO();
       final Thread pt = dao.findByUID( 1 );
       pt.setID( 100 );
       dao.updateThread(pt);
       fail("Expect ThreadNotFoundException");
   }catch( final SQLException sqlException ){
       fail("Unexpected excetpion: " + sqlException );
   }catch( final ThreadNotFoundException threadNotFoundException ){
       //Pass
   }
       
}

public void testUpdateThreadNormal() throws Exception {
   ThreadDAO dao = new ThreadDAO();
   Thread pt = dao.findByUID(1);
   // store old values
   String oldTitle = pt.getTitle();
   String oldContent = pt.getContent();
   boolean oldNotify = pt.getNotify();
   // change values
   pt.setTitle ( oldTitle + "AddedTitle.");
   pt.setContent ( oldContent+ "AddedContent.");
   pt.setNotify(!oldNotify);
   // update
   dao.updateThread(pt);
   
   // query again
   pt = dao.findByUID(1);
   
   assertEquals("Update Thread Title", oldTitle + "AddedTitle.", pt.getTitle());
   assertEquals("Update Thread Content", oldContent + "AddedContent.", pt.getContent());
   assertEquals("Update thread Notify", !oldNotify, pt.getNotify());
   
   // back to original value
   // change values
   pt.setTitle( oldTitle );
   pt.setContent( oldContent );
   pt.setNotify( oldNotify );
   // update
   dao.updateThread(pt);
}
   编译,运行测试,一定失败,呵呵,这就是现在我们的目标,测试框架已经完成

3. 开始写 ThreadDAO.updateThread() 方法

   /**
    * Update title , content, notify of a thread
    */
   public void updateThread( final Thread thread )
           throws SQLException, ThreadNotFoundException{
       Validation.validateNotNull( thread );
       final Thread newThread = thread;
       findByUID( newThread.getID() );
       Connection conn = getConnection();
       PreparedStatement stat
           = conn.prepareStatement( "update " + ... );
       stat.setString( 1, newThread.getTitle() );
       stat.setString( 2, newThread.getContent() );
       stat.setString( 3, notifyString );
       stat.execute();

       stat.close();
       conn.close();
   }

   然后运行测试,如果测试失败,继续修改代码,直到测试成功。

4. 如果以后对代码做修改,比如方法内部的优化。。。只要重新运行测试,
  就可以很有信心的说,我的代码没有问题。


这有几个好处
 写程序是可以知道什么时候写代码结束了,以运行测试是否成功为标准
 对自己的代码很有信心,特别是对将来的改动,不用害怕。
 好的代码是可测试的代码,写测试的过程,会发现原来设计中不足的地方,可加以修改
 测试可以作为代码的文档,要知道代码作了些什么,看测试代码就可以了。

测试工具:
 JUnit(单元测试框架类库)
 Ant(Java 项目管理软件)

 
designlee 发表于: 2002/10/08 03:49pm
好长,偶头晕了,先收起来!
 
eclipse 发表于: 2002/10/08 03:55pm
看完了,可惜功力不够,感觉理解只是字面上的,again~~ :)
 
cinc 发表于: 2002/10/08 04:24pm
最上面那篇文章我也没仔细看,太多理论了,看不是很懂。

不过 XP 中提到的很多实践(象 test first design, make it simple, refactoring)
却是很实际很实际的东西。都是使用后立马见效的。可以说也是前人的经验积累,学习他
们对自己的编程水平的提高很有帮助。

我也是在真正做过一次之后才真正有些了解,到理解和熟练还有好长一段路要走哦。

大家一起努力实践。呵呵。
:)

 
 

Copyright © ChinaUnix.net  *  转载请注明出处及作者