过去几年的经验告诉我:单元测试已然是“被解决的问题”了。所有的信息、图书、工具都摆在面前,你只要把NUnit拣起来可以上路了,不是么?

  不是。

  即便是在下决心要开始写单元测试之前,我们也得从别人那里吸取经验,从那些好的坏的故事里,那些令人绝望或是见证奇迹(一个测试省了我一周时间!)的时刻中,取其精华弃其糟粕。即便这样,等我们勇敢上路之后还会意识到,要学的东西还多着呢!

  我想跟你讲讲我在单元测试这片大陆上一段奇妙的旅程。我们Typemock的团队已经在这块大陆上游历了数年,这些经历也改变了我们的产品开发过程。Isolator是我们的主打产品,它开始是作为mock框架出现的,但是当我们对在真实世界中单元测试的问题了解的越来越多,我们开始开发一些特性帮人们解决这些问题。直到现在,还有很多事情没搞定。

  不过我们还是从头讲起吧。Typemock有一个简单的信念:让单元测试变得很容易。

  够简单吧,可是容易么?

  呵呵……

  写单元测试可不是件容易的事情。单元测试的好处数不胜数,这大家都明白。但你得好好加把劲,才能享受到这些好处。

  咱们都有个代码库。有些幸运的家伙会面对一块未开垦的处女地,但更多的人却会遇到大量“遗留代码”,这才是常态。我们写测试的时候,测的是那些遗留代码。这真的很棘手啊。

  Typemock刚刚起步的时候,不修改代码给遗留代码写测试还是件不可能的事情。但这正是Isolator的主要目标:在不修改代码的情况下编写单元测试。当Isolator能够mock每一种.NET对象类型,给遗留代码写单元测试已成为可能。

  演化中的API

  随着时间推移,我们明白了要好好控制API。开始的一版API是基于string的,比如要伪造DateTime.Now的时候,你需要这样写:

Mock mockDateTime = MockManager.MockAll<DateTime>();
mockDateTime.ExpectGetAlways("Now", new DateTime(2000, 1, 1));

  看上去不太漂亮,但是管用。然而这些代码稍一重构会废掉。所以我们换成了录制-重放(record-replay)模型,对重构的支持友好一些了,虽然看上去有些怪异:

using (RecordExpectations recorder = RecorderManager.StartRecording())
{
    DateTime.Now = new DateTime(2000, 1, 1);
}

  这一版API算得上是一次革命性的飞跃,但是录制-重放模型已经过时了,而且这个版本还有些技术问题需要解决。所以当lambda表达式出现以后,我们的API又为了保证可读性和支持重构来了个华丽的转身:

Isolate.WhenCalled(() => DateTime.Now).WillReturn(new DateTime(2000, 1, 1));

  在当前这个版本中,我们还做了另一个简化,用“fake”换掉了“mock”这个词汇,“mock”和“stub”被用的太多了,而且总是被滥用,被误解。为了避免麻烦,我们决定回避这个问题,省得还要把mock和stub之间所有的细微差别给新手一一讲述。

  贴心的邻居

  Isolator不只是Visual Studio的插件而已──我们得让它跟其他工具和供应商集成。代码覆盖率,性能分析器,构建引擎等等,不管是啥,只要你能想得到。Isolator需要良好的兼容性,这样大家能在不同的配置下用不同的工具跑测试。

  说到跑测试,脱离Visual Studio跑怎么样?当你开始做自动化构建的工作以后,你会学到很多MS家族中琳琅满目的工具,当然也包括全能的TFS大神。Isolator在分析器上做了大量的集成工作,让测试能被纳入持续集成流程中运行。因为不同的团队会用不同的工具集和CI服务器,为了保证在不同环境下的适用性,我们是花了不少力气的。

  健壮的API

  随便找个考虑过写单元测试的人来问问,她都会忧郁地跟你说:我的代码要改,可我不想每次都要改测试。你能帮帮我么?

  能力越大责任越大,这句话放到Mock框架上也一点没错(蜘蛛侠也是一样)。改变行为的能力来源于了解对象内部的行为。而这种如同X射线一般的功能,也正是它的阿喀琉斯之踵──改变内部代码同样也会影响测试。