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):
- public class Parse {
- public String leader;
- public String tag;
- public String body;
- public String end;
- public String trailer;
- public Parse more;
- public Parse parts;
- public Parse (String tag, String body, Parse parts, Parse more) {
- this.leader = "\n";
- this.tag = "<"+tag+">";
- this.body = body;
- this.end = ""+tag+">";
- this.trailer = "";
- this.parts = parts;
- this.more = more;
- }
- public static String tags[] = {"table", "tr", "td"};
- public Parse (String text) throws ParseException {
- this (text, tags, 0, 0);
- }
- public Parse (String text, String tags[]) throws ParseException {
- this (text, tags, 0, 0);
- }
- public Parse (String text, String tags[], int level, int offset) throws ParseException {
- String lc = text.toLowerCase();
- int startTag = lc.indexOf("<"+tags[level]);
- int endTag = lc.indexOf(">", startTag) + 1;
- // int startEnd = lc.indexOf(""+tags[level], endTag);
- int startEnd = findMatchingEndTag(lc, endTag, tags[level], offset);
- int endEnd = lc.indexOf(">", startEnd) + 1;
- int startMore = lc.indexOf("<"+tags[level], endEnd);
- if (startTag<0 || endTag<0 || startEnd<0 || endEnd<0) {
- throw new ParseException ("Can't find tag: "+tags[level], offset);
- }
- leader = text.substring(0,startTag);
- tag = text.substring(startTag, endTag);
- body = text.substring(endTag, startEnd);
- end = text.substring(startEnd,endEnd);
- trailer = text.substring(endEnd);
- if (level+1 < tags.length) {
- parts = new Parse (body, tags, level+1, offset+endTag);
- body = null;
- }
- else { // Check for nested table
- int index = body.indexOf("<" + tags[0]);
- if (index >= 0) {
- parts = new Parse(body, tags, 0, offset + endTag);
- body = "";
- }
- }
- if (startMore>=0) {
- more = new Parse (trailer, tags, level, offset+endEnd);
- trailer = null;
- }
- }
- ...
- }
Fixture遍历解析出来的Parse对象,即HTML中所有的表,把表中的内容与实际的结果进行比较。
- public class Fixture {
- ...
- // Traversal //////////////////////////
- /* Altered by Rick Mugridge to dispatch on the first Fixture */
- public void doTables(Parse tables) {
- summary.put("run date", new Date());
- summary.put("run elapsed time", new RunTime());
- if (tables != null) {
- Parse fixtureName = fixtureName(tables);
- if (fixtureName != null) {
- try {
- Fixture fixture = getLinkedFixtureWithArgs(tables);
- fixture.interpretTables(tables);
- } catch (Exception e) {
- exception (fixtureName, e);
- interpretFollowingTables(tables);
- }
- }
- }
- }
- ...
- public void doTable(Parse table) {
- doRows(table.parts.more);
- }
- public void doRows(Parse rows) {
- while (rows != null) {
- Parse more = rows.more;
- doRow(rows);
- rows = more;
- }
- }
- public void doRow(Parse row) {
- doCells(row.parts);
- }
- public void doCells(Parse cells) {
- for (int i=0; cells != null; i++) {
- try {
- doCell(cells, i);
- } catch (Exception e) {
- exception(cells, e);
- }
- cells=cells.more;
- }
- }
- public void doCell(Parse cell, int columnNumber) {
- ignore(cell);
- }
-
- ...
- }
Fixture的doTables方法处理所有的表,根据表名生成相应的Fixture(子类)对象:“Fixture fixture =
getLinkedFixtureWithArgs(tables);”。之后便调用生成的Fixture对象的doTable方法来处理它负责的表,进
而调用doRows, doRow, doCells, doCell等方法。例如上面的表格eg.ArithmeticColumnFixture,它能找到相应的Fixture类:
- package eg;
- // Copyright (c) 2002 Cunningham & Cunningham, Inc.
- // Released under the terms of the GNU General Public License version 2 or later.
- import fit.*;
- public class ArithmeticFixture extends PrimitiveFixture {
- int x=0;
- int y=0;
- public void doRows(Parse rows) {
- super.doRows(rows.more); // skip column heads
- }
- public void doCell(Parse cell, int column) {
- switch (column) {
- case 0: x = (int)parseLong(cell); break;
- case 1: y = (int)parseLong(cell); break;
- case 2: check(cell, x+y); break;
- case 3: check(cell, x-y); break;
- case 4: check(cell, x*y); break;
- case 5: check(cell, x/y); break;
- default: ignore(cell); break;
- }
- }
- }
Fixture子类只需覆盖它感兴趣的方法(比如doCell),FIT框架会负责调用它们。上面看到Fixture的check方法,它的实现如下:
- public class Fixture {
- ...
- public void check(Parse cell, TypeAdapter a) {
- String text = cell.text();
- if (text.equals("")) {
- try {
- info(cell, a.toString(a.get()));
- } catch (Exception e) {
- info(cell, "error");
- }
- } else if (a == null) {
- ignore(cell);
- } else if (text.equals("error")) {
- try {
- Object result = a.invoke();
- wrong(cell, a.toString(result));
- } catch (IllegalAcces***ception e) {
- exception (cell, e);
- } catch (Exception e) {
- right(cell);
- }
- } else {
- try {
- Object result = a.get();
- if (a.equals(a.parse(text), result)) {
- right(cell);
- } else {
- wrong(cell, a.toString(result));
- }
- } catch (Exception e) {
- exception(cell, e);
- }
- }
- }
- public void right (Parse cell) { cell.addToTag(" bgcolor=\"" + green + "\""); counts.right++; }
- public void wrong (Parse cell) { cell.addToTag(" bgcolor=\"" + red + "\""); cell.body = escape(cell.text()); counts.wrong++; }
- public void wrong (Parse cell, String actual) { wrong(cell); cell.addToBody(label("expected") + "" + escape(actual) + label("actual")); }
- ...
- }
Check方法利用TypeAdapter对象让传入的值(比如x*y)与表格单元内的期望值进行比较,如果结果正确则调用right方法把表格单元变绿,否则就把表格单元变红。TypeAdapter类的(部分)定义为:
- public class TypeAdapter {
- ...
- public Object get() throws IllegalAcces***ception, InvocationTargetException {
- if (field != null) {return field.get(target);}
- if (method != null) {return invoke();}
- return null;
- }
- public void set(Object value) throws IllegalAcces***ception {
- field.set(target, value);
- }
- ...
- }
FIT同样提供了很多定义后的TypeAdapter,比如上面表格中便使用到了IntAdapter:
- public class TypeAdapter {
- static class IntAdapter extends ClassIntegerAdapter {
- public void set(Object i) throws IllegalAcces***ception {
- field.setInt(target, ((Integer)i).intValue());
- }
- }
- }
至此,三个核心类便都介绍完了。为了让这个框架运行起来,我们需要运行FIT提供的fit.jar:
java -classpath fit.jar fit.FileRunner examples/input/arithmetic.html results.html
FileRunner是一个java类,它接受输入和输出两个参数,下面是它的(部分)实现:
- public class FileRunner {
- public String input;
- public Parse tables;
- public Fixture fixture = new Fixture();
- public PrintWriter output;
- public static void main(String argv[]) {
- try {
- new FileRunner().run(argv);
- }
- catch (Exception e) {
- System.err.println(e.getMessage());
- System.exit(-1);
- }
- }
- public void run(String argv[]) throws IOException {
- args(argv);
- process();
- exit();
- }
- public void process() {
- try {
- if (input.indexOf("") >= 0) {
- tables = new Parse(input, new String[]{"wiki", "table", "tr", "td"});
- fixture.doTables(tables.parts);
- } else {
- tables = new Parse(input, new String[]{"table", "tr", "td"});
- fixture.doTables(tables);
- }
- } catch (Exception e) {
- exception(e);
- }
- tables.print(output);
- }
- ...
- }
可以看到,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》。
阅读(875) | 评论(0) | 转发(0) |