单元测试中的“单元‘如何定义?

很多人做过单元测试,可能对单元定义没有较真过,其实普通小名词蕴含大概念。

Martin Fowler在其 “单元测试”一文中对单元定义是一个类,一个类中可能有很多方法行为,单元不能粒度太细,也不能太粗,太细了就容易范DHH批评的宗教原教旨主义,也就是形式主义,影响了正常软件设计意图,太粗了则达不到持续交付的目标,软件BUG频出。

这场讨论是来自Rails之父DHH提出“TDD已死”的讨论,主要是针对单元测试在Web框架比较难以实施,因为普通Web框架中就是一个MVC框架,Model实际是数据库,在MVC结构下单元测试无法插足,一般是使用变种的Controller,这使得C控制器的含义变成两个,一个是MVC的控制器,用于数据库模型Model和视图View的交互;另外一个专门用于对模型Model测试的,”TDD已死“派认为这样不妥,破坏了控制器原本设计含义。

其实,banq我个人意见还是坚持以前的想法,这种尴尬是因为MVC这种不合时宜的结构模式造成的,我2010年曾经写了一篇:MVC模式已死,里面实际谈到了MVC模式的僵化,建议以DCI等架构,当然包括后来的DDD/CQRS来替代,从今天观点来看,这样才能真正避免MVC模式和单元测试的冲突。

Ruby阵营的另外一位牛人Andrzej Krzywda发表了TDD and Rails - what makes a good Unit?更加佐证了我当初的观点。

在这篇文章中,AK认为单元的定义需要从你采取的技术架构上定义,在你应该知道的四种架构一文中已经谈及了六边形Hexagonal架构 干净的架构和DDD/CQRS以及DCI。

AK举了一个例子,如果你有订单Order, 一个订单中有很多订单条目OrderLines, 以及一个货运地址ShippingAddress和一个客户Customer. 那么你是否为这四个对象建立四个单元测试呢?很显然不是,而是只要为Order这个类建立一个单元测试就可以了,为什么呢?因为通过Order对象测试已经可以掌握整体了。测试可能从未知道ShippingAddress的存在。因为这是单元Order的内部实现细节。

所以,一个类并不是一定是一个好的单元,通常是一组类。单元的定义是为了让你测试,而不管被测试单元内部如何变化,这是单元测试的设计目标,这样你在每次重构时就不必改变测试代码了。

从DDD角度看,Order其实是一个聚合根,也就是说,单元测试中单元是指为每个聚合根做一个测试就可以了。一个DDD aggregate聚合是一组领域对象,能够被看成一个单元就可以了。

在“干净的架构”中有一个用例的概念有所涉及,是通过一组操作来定义的。

在“六边形架构”中有关于一个单元周围被适配器adapters围绕, 这个单元通常称为Middle Hex。

在“DCI”架构中,DHH引述James Coplien的TDD的谈话。James已经小有名气,不仅从他对TDD有过强烈推动和意见,而且更多的他在DCI世界的活动。他是这项运动的创始人之一。 DCI是这里最鼓舞人心的架构。Ruby和DCI其实是一个梦幻般的组合。 DCI提供了良好的工具用于定义一个单位是什么。定义一个上下文为一个单元,一个单元是一组对象的协作的范式。

[该贴被banq于2014-05-08 09:29修改过]

作为Ruby的MVC框架Rails创建者之所以提出TDD已死,其中一个原因可能是他已经深刻感受到MVC与TDD的矛盾之处,这种矛盾如同对象和关系数据库,已经到了水火不容,不可调和地步,也许只有宣布一方死亡才可保持另外一方的存在。

其实随着后端RESTful架构和微服务的兴起,SpringMVC这种昔日MVC框架其实已经褪化成了REST框架,而前端MVC框架AngularJS的兴起正好说明了这种趋势。

可能的情况下,Rails这种Ruby MVC框架如同SpringMVC,虽然打着MVC框架名头,实际其已经是一个RESTful框架,其控制器已经不是协调Model和View之间的调节者,而是一个响应REST中POST/GET等动作的处理器Handler,在这个处理器中实现对领域业务层的单元测试,就相当自然和优雅了。

在Bob大叔的干净的架构中,他提到MVC是作为适配器层,对业务规则的实体用例与外部Web进行适配,其实这个定义扭曲了MVC本来的定义,而REST的处理器作为适配器层是最恰当的。

/**
* 保存一个操作步骤到队列中去
* $skip = new Skip();
$skip->setActionResult("同意");
$skip->setNextStep(5);

$skip2 = new Skip();
$skip2->setActionResult("不同意");
$skip2->setNextStep(3);

$as = new ActionStep();
$as->addSkip($skip);
$as->addSkip($skip2);
$as->setActionClassName("TwoResultAction");
$as->setPosition("副支队长@支队领导");
$as->setStepName("支队分管审批");
$as->setStepNum(4);

$que = new ActionQueue(1);
$que->addActionStep($as);

echo R::store($que->getBean());

==========================
现实中,很自然的以聚合根作为基本测试单元。
最原始的丑陋测试代码,没有用测试用例,白盒测试,自动化什么的。