Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1737360
  • 博文数量: 438
  • 博客积分: 9799
  • 博客等级: 中将
  • 技术积分: 6092
  • 用 户 组: 普通用户
  • 注册时间: 2012-03-25 17:25
文章分类

全部博文(438)

文章存档

2019年(1)

2013年(8)

2012年(429)

分类: Java

2012-03-27 13:16:51

Michael Feathers

通常我们在进行软件设计时,总能从前辈或同仁那学习或自己发现一些很好的经验,比如尽量用委托而不要用继承;不要使用公有变量等等。在大多数情况下,这些宝贵的经验确实给我们带来了不少好处。但是它们只是原则,而不是教条,在需要的时候还是可以变通。


Ward Cunningham在2002年发布的自动测试框架——FIT,便是另类却优雅的例子。它几乎无视Java中的一切设计原则。


FIT框架以一个HTML文档为输入,它会找到文档里面所有的表格,并以这些表格作为基准。之后FIT用这些基准与被测试的程序的运行的实际结果比对,将比对结果输出到一个HTML文档里。


比如,下面这个表格为输入:

eg.ArithmeticColumnFixture
x y plus() times() divide() floating()
2 3 5 6 0 0.6666667
0 0 0 0 error error
0 0 0 0    
200 300 500 60000 0 0.6666667
2 3 10 10 10
200 3 5 6 0 0.6666667
2 -3 -1 -6 -0 -0.6666667

 与运行结果比对后,得到输出:

eg.ArithmeticColumnFixture
x y plus() times() divide() floating()
2 3 5 6 0 0.6666667
0 0 0 0 error error expected NaN actual
0 0 0 0 error NaN
200 300 500 60000 0 0.6666667
2 3 10 expected 5 actual 10 expected 6 actual 10 expected 0 actual
200 3 5 expected 203 actual 6 expected 600 actual 0 expected 66 actual 0.6666667 expected 66.666664 actual
2 -3 -1 -6 -0 -0.6666667

 

这是一个很有用而且很强大的框架,但它的核心代码却很短小精悍,只有三个类:Parse、Fixture和TypeAdapter


Parse类对应于HTML的(表格)元素,从HTML文档中解析出来的元素,比如表格table,表格行row,表格单元cell都是一个Parse对象。Parse类用parts属性表示它的子元素,而用more属性表示它的兄弟元素。下面是Parse类的(部分)实现(Java):


  1. public class Parse {

  2.     public String leader;
  3.     public String tag;
  4.     public String body;
  5.     public String end;
  6.     public String trailer;

  7.     public Parse more;
  8.     public Parse parts;

  9.     public Parse (String tag, String body, Parse parts, Parse more) {
  10.         this.leader = "\n";
  11.         this.tag = "<"+tag+">";
  12.         this.body = body;
  13.         this.end = "+tag+">";
  14.         this.trailer = "";
  15.         this.parts = parts;
  16.         this.more = more;
  17.     }

  18.     public static String tags[] = {"table", "tr", "td"};

  19.     public Parse (String text) throws ParseException {
  20.         this (text, tags, 0, 0);
  21.     }

  22.     public Parse (String text, String tags[]) throws ParseException {
  23.         this (text, tags, 0, 0);
  24.     }

  25.     public Parse (String text, String tags[], int level, int offset) throws ParseException {
  26.         String lc = text.toLowerCase();
  27.         int startTag = lc.indexOf("<"+tags[level]);
  28.         int endTag = lc.indexOf(">", startTag) + 1;
  29. // int startEnd = lc.indexOf("
  30.         int startEnd = findMatchingEndTag(lc, endTag, tags[level], offset);
  31.         int endEnd = lc.indexOf(">", startEnd) + 1;
  32.         int startMore = lc.indexOf("<"+tags[level], endEnd);
  33.         if (startTag<0 || endTag<0 || startEnd<0 || endEnd<0) {
  34.             throw new ParseException ("Can't find tag: "+tags[level], offset);
  35.         }

  36.         leader = text.substring(0,startTag);
  37.         tag = text.substring(startTag, endTag);
  38.         body = text.substring(endTag, startEnd);
  39.         end = text.substring(startEnd,endEnd);
  40.         trailer = text.substring(endEnd);

  41.         if (level+1 < tags.length) {
  42.             parts = new Parse (body, tags, level+1, offset+endTag);
  43.             body = null;
  44.         }
  45.         else { // Check for nested table
  46.             int index = body.indexOf("<" + tags[0]);
  47.             if (index >= 0) {
  48.                 parts = new Parse(body, tags, 0, offset + endTag);
  49.                 body = "";
  50.             }
  51.         }

  52.         if (startMore>=0) {
  53.             more = new Parse (trailer, tags, level, offset+endEnd);
  54.             trailer = null;
  55.         }
  56.     }

  57.     ...
  58. }

Fixture遍历解析出来的Parse对象,即HTML中所有的表,把表中的内容与实际的结果进行比较。


  1. public class Fixture {

  2.     ...

  3.     // Traversal //////////////////////////

  4.     /* Altered by Rick Mugridge to dispatch on the first Fixture */
  5.     public void doTables(Parse tables) {
  6.         summary.put("run date", new Date());
  7.         summary.put("run elapsed time", new RunTime());
  8.         if (tables != null) {
  9.             Parse fixtureName = fixtureName(tables);
  10.             if (fixtureName != null) {
  11.                 try {
  12.                     Fixture fixture = getLinkedFixtureWithArgs(tables);
  13.                     fixture.interpretTables(tables);
  14.                 } catch (Exception e) {
  15.                     exception (fixtureName, e);
  16.                     interpretFollowingTables(tables);
  17.                 }
  18.             }
  19.         }
  20.     }

  21.     ...

  22.     public void doTable(Parse table) {
  23.         doRows(table.parts.more);
  24.     }

  25.     public void doRows(Parse rows) {
  26.         while (rows != null) {
  27.             Parse more = rows.more;
  28.             doRow(rows);
  29.             rows = more;
  30.         }
  31.     }

  32.     public void doRow(Parse row) {
  33.         doCells(row.parts);
  34.     }

  35.     public void doCells(Parse cells) {
  36.         for (int i=0; cells != null; i++) {
  37.             try {
  38.                 doCell(cells, i);
  39.             } catch (Exception e) {
  40.                 exception(cells, e);
  41.             }
  42.             cells=cells.more;
  43.         }
  44.     }

  45.     public void doCell(Parse cell, int columnNumber) {
  46.         ignore(cell);
  47.     }

  48.  
  49.     ...
  50. }

Fixture的doTables方法处理所有的表,根据表名生成相应的Fixture(子类)对象:“Fixture fixture = getLinkedFixtureWithArgs(tables);”。之后便调用生成的Fixture对象的doTable方法来处理它负责的表,进 而调用doRows, doRow, doCells, doCell等方法。例如上面的表格eg.ArithmeticColumnFixture,它能找到相应的Fixture类:


  1. package eg;

  2. // Copyright (c) 2002 Cunningham & Cunningham, Inc.
  3. // Released under the terms of the GNU General Public License version 2 or later.

  4. import fit.*;

  5. public class ArithmeticFixture extends PrimitiveFixture {

  6.     int x=0;
  7.     int y=0;

  8.     public void doRows(Parse rows) {
  9.         super.doRows(rows.more); // skip column heads
  10.     }

  11.     public void doCell(Parse cell, int column) {
  12.         switch (column) {
  13.             case 0: x = (int)parseLong(cell); break;
  14.             case 1: y = (int)parseLong(cell); break;
  15.             case 2: check(cell, x+y); break;
  16.             case 3: check(cell, x-y); break;
  17.             case 4: check(cell, x*y); break;
  18.             case 5: check(cell, x/y); break;
  19.             default: ignore(cell); break;
  20.         }
  21.     }
  22. }

Fixture子类只需覆盖它感兴趣的方法(比如doCell),FIT框架会负责调用它们。上面看到Fixture的check方法,它的实现如下:


  1. public class Fixture {
  2.     ...

  3.     public void check(Parse cell, TypeAdapter a) {
  4.         String text = cell.text();
  5.         if (text.equals("")) {
  6.             try {
  7.                 info(cell, a.toString(a.get()));
  8.             } catch (Exception e) {
  9.                 info(cell, "error");
  10.             }
  11.         } else if (a == null) {
  12.             ignore(cell);
  13.         } else if (text.equals("error")) {
  14.             try {
  15.                 Object result = a.invoke();
  16.                 wrong(cell, a.toString(result));
  17.             } catch (IllegalAcces***ception e) {
  18.                 exception (cell, e);
  19.             } catch (Exception e) {
  20.                 right(cell);
  21.             }
  22.         } else {
  23.             try {
  24.                 Object result = a.get();
  25.                 if (a.equals(a.parse(text), result)) {
  26.                     right(cell);
  27.                 } else {
  28.                     wrong(cell, a.toString(result));
  29.                 }
  30.             } catch (Exception e) {
  31.                 exception(cell, e);
  32.             }
  33.         }
  34.     }


  35.     public void right (Parse cell) { cell.addToTag(" bgcolor=\"" + green + "\""); counts.right++; }

  36.     public void wrong (Parse cell) { cell.addToTag(" bgcolor=\"" + red + "\""); cell.body = escape(cell.text()); counts.wrong++; }

  37.     public void wrong (Parse cell, String actual) { wrong(cell); cell.addToBody(label("expected") + "" + escape(actual) + label("actual")); }
  38.     ...
  39. }

Check方法利用TypeAdapter对象让传入的值(比如x*y)与表格单元内的期望值进行比较,如果结果正确则调用right方法把表格单元变绿,否则就把表格单元变红。TypeAdapter类的(部分)定义为:


  1. public class TypeAdapter {
  2.     ...
  3.     public Object get() throws IllegalAcces***ception, InvocationTargetException {
  4.         if (field != null) {return field.get(target);}
  5.         if (method != null) {return invoke();}
  6.         return null;
  7.     }

  8.     public void set(Object value) throws IllegalAcces***ception {
  9.         field.set(target, value);
  10.     }
  11.     ...
  12. }

FIT同样提供了很多定义后的TypeAdapter,比如上面表格中便使用到了IntAdapter:


  1. public class TypeAdapter {
  2.     static class IntAdapter extends ClassIntegerAdapter {
  3.         public void set(Object i) throws IllegalAcces***ception {
  4.             field.setInt(target, ((Integer)i).intValue());
  5.         }
  6.     }
  7. }

至此,三个核心类便都介绍完了。为了让这个框架运行起来,我们需要运行FIT提供的fit.jar:

java -classpath fit.jar fit.FileRunner examples/input/arithmetic.html results.html

FileRunner是一个java类,它接受输入和输出两个参数,下面是它的(部分)实现:


  1. public class FileRunner {

  2.     public String input;
  3.     public Parse tables;
  4.     public Fixture fixture = new Fixture();
  5.     public PrintWriter output;

  6.     public static void main(String argv[]) {
  7.         try {
  8.             new FileRunner().run(argv);
  9.         }
  10.         catch (Exception e) {
  11.             System.err.println(e.getMessage());
  12.             System.exit(-1);
  13.         }
  14.     }

  15.     public void run(String argv[]) throws IOException {
  16.         args(argv);
  17.         process();
  18.         exit();
  19.     }

  20.     public void process() {
  21.         try {
  22.             if (input.indexOf("") >= 0) {
  23.                 tables = new Parse(input, new String[]{"wiki", "table", "tr", "td"});
  24.                 fixture.doTables(tables.parts);
  25.             } else {
  26.                 tables = new Parse(input, new String[]{"table", "tr", "td"});
  27.                 fixture.doTables(tables);
  28.             }
  29.         } catch (Exception e) {
  30.             exception(e);
  31.         }
  32.         tables.print(output);
  33.     }

  34.     ...
  35. }

可以看到,FileRunner同时使用了Parse和Fixture来得到想要的结果。

FIT框架之所以另类,是因为它几乎开放了它的所有的成员属性与方法。这种开放性使得代码小巧且容易理解。FIT之所以这样选择,是因为可扩展性对于它来 说并没那么重要,灵活和简洁才是它所追求的。(毕竟,HTML的格式不会轻易地变化。)此外,Parse类负责了整个HTML的解析工作,而且单凭它的构 造函数便负责构造起一棵树。这样的设计也是FIT代码另一个优美的地方。


:Object Mentor的资深成员,为极限编程(Agile/XP programming)、测试驱动开发(test-driven development)、重构(refactoring)、面向对象设计(object-oriented design),  Java, C#, 和 C++提供培训服务。代表作有《Working Effectively with Legacy Code》。

 

阅读(816) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~