从“用断言检查 null”到“设计单元测试”

  • 李剑

2009 年 3 月 12 日

话题:Java敏捷.NET语言 & 开发架构文化 & 方法

防御式编程是保护自己的程序不受外部侵害的一种有效方式。代码大全在“防御式编程”这一章中讲到,可以用断言(assert)来检查真实情况是否满足自己的预期,例如指针非空。

上个月Miško Hevery写了一篇文章:断言,还是不断言,描述了在构造器中编写断言给单元测试所造成的种种不便。

他先举了一个实际例子:

class House {
  Door door;
  Window window;
  Roof roof;
  Kitchen kitchen;
  LivingRoom livingRoom;
  BedRoom bedRoom;

  House(Door door, Window window,
            Roof roof, Kitchen kitchen,
            LivingRoom livingRoom,
            BedRoom bedRoom){
    this.door = Assert.notNull(door);
    this.window = Assert.notNull(window);
    this.roof = Assert.notNull(roof);
    this.kitchen = Assert.notNull(kitchen);
    this.livingRoom = Assert.notNull(livingRoom);
    this.bedRoom = Assert.notNull(bedRoom);
  }

  void secure() {
    door.lock();
    window.close();
  }
}

然后说到,因为 secure 方法只需要 door 和 window 两个对象,所以理所当然的是,在测试这个方法的时候只需要初始化 door 和 window,其他参数用 null 代替,如: 、

House house = new House(door, window,null, null, null, null);

这样的方法才能够让人看明白意图,但是因为构造器中做了参数非空的断言,所以就不得不这样初始化 House 对象:

House house = new House(door, window,

    new Roof(),

    new Kitchen(),

    new LivingRoom(),

    new BedRoom());

这样就让人不太能看得懂到底是哪些对象真正在测试方法中被用到了。而当构造器中的参数增多,代码的可读性就会更差。Miško 最后提到:

我不是反对断言,我也在自己的代码中也常用,不过我的大多数断言只是用来检查对象的内部状态,而不是是不是传入了一个 null 值。检查是不是 null 往往会影响到代码的测试,如果要我在完好测试过的代码和有断言但是没测试的代码之间做选择,结果是不言而喻的。

有不少人跟帖反对 Miško 的观点,他们认为这根本不是断言的问题,而是来自于设计本身,例如

Rasmus Kromann-Larsen就提出,

你为啥不用 mock 框架而非要自己初始化那么一堆东西呢?在.NET 世界里面有个新词,叫做 automocker——你可以把它看作是 mock 框架和 ioc 容器的混合体……

看看这个链接吧:http://blog.eleutian.com/CommentView,guid,762249da-e25a-4503-8f20-c6d59b1a69bc.aspx

Howie则说:

不管是我写的代码,还是我看到的代码,都是在调用对象的时候去做断言,而不是在保存对象的时候。这样合理不?所以你应该在 secure() 方法里面写 Assert.notNull(door);Assert.notNull(window)。用到的时候再去检查,而不是提前 就把一切都检查好,以备过几天以后需要用。

zdsbs提出用 Builder 来取代构造器:

Door door = new Door();

Window window = new Window();

House house = HouseBuilder.new().with(door).with(window);

house.secure();

assertTrue(door.isLocked());



assertTrue(window.isClosed());

这样问题就很好的解决了,其他变量都可以在 HouseBuilder 里面提供一些缺省值,你也不会创建出具有非法状态的对象来。

earlNameless对 Miško 的设计思路直接抨击说:

我不同意你的看法,因为你这个例子里面,人们得先知道你所测试的方法只用到了 door 和 window,没用其他任何东西。

所以你的测试人员就得了解方法的具体实现,而这是不应该的……我如果有个可以滑开的房顶,我就得还给它上锁,可现在房顶是 null。

后面有几个人同意 earlNameLess 的看法:测试代码需要清楚的了解实现代码的内部实现方式实在不是什么好做法。建议根据具体情况提取出接口,或者用一下 Ioc 容器。

不过也有些人对 earlNameless 的看法表示反对,例如 Adam Tybor说到:

为了写测试,不就是得知道实现方式么。这是 TDD,不是 TAD,不是么?如果房顶还需要加锁,那就让测试失败,然后改实现,直到所有测试变绿。

Christian Gruber说:

上面有很多讨论都是针对 Misko 的测试代码不得不知道实现细节这个做法的。虽然,测试代码了解内部细节是有点不太稳定,但这是单元测试,单元测试是白盒 测试,不是黑盒。如果你用 mock,那你肯定得知道内部细节……再说的清楚一点,你是在测试基于给定的条件,你的代码能够得到预期的结果。你必须得知道这 些条件是啥,把那些不满足的条件排除掉。

如果有人来改了实现细节,那测试就会失败。不过这也暴露出一个问题来,你必须得有一整套单元测试可以做回顾。如果我改了行为,我也就修改了测试的意图。然后我就得修改测试,让它对新的假设起作用。

所以这要看你是想测试行为还是输入输出,这已经是设计测试的哲学范畴了……

跑题跑了一阵子以后,rnaufal回到原来话题上,直接抛出了 JDK 文档:

JDK 文档中有一篇的标题是“用断言编程”,链接在这里:“http://java.sun.com/j2se/1.4.2/docs/guide/lang/assert.html#usage”

里面写着:不要在 public 方法中用断言做变量检查。

读者朋友,你在自己的代码中使用防御式编程么?用过 assert 来检查 null 与否么?你的单元测试是在检查行为,还是检查输入输出呢?欢迎分享你们的意见。

Java敏捷.NET语言 & 开发架构文化 & 方法