您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
架构之路(三):测试驱动与忘记数据库
 
作者:自由飞 来源: CSDN 火龙果软件  发布于 2015-10-21
  1220  次浏览      34
 

摘要:测试驱动有着更宽广的概念,他要求以“测试”为驱动力,来推动整个开发活动。似乎相当多的人认为这个观点根本不具有可执行性。但当我第一眼接触这个观点,我就觉得,它像一道闪电划破长空,光华璀璨,价值无以伦比!

【编者按】本文作者自由飞是一个奇人, 彻彻底底的非科班程序员:98年读大学-国际贸易专业、03年11月英语培训机构当英语老师、04年2月-05年6月律师事务所实习和公司法务、05年6月-07年12月成立装饰公司做老板、08年8月软件公司做程序员。

上一章我们提到, 单元测试只是测试驱动的一个子集;换言之,测试驱动有着更宽广的概念,他要求以“测试”为驱动力,来推动整个开发活动。这个观点似乎非常具有争议性,相当多的人认为其根本不具有可执行性。但很奇怪的是,当我第一眼接触这个观点,我就觉得,它像一道闪电划破长空,它光华璀璨,价值无以伦比!

需求文档可测试化

我第一点想到的,就是需求文档应该可测试化。我不太明白,这样简单有效的一个工作,为什么几乎没有人去做?因为发包方的原因?

大家接触到的需求文档是什么样子的?我从来没有看到过一份我满意的需求文档。都是word格式的,文字加上图片说明。文字都是简单的陈述句说明,比如:用户名不能重复。简明扼要,是不是?是你妈个逼!sorry,我爆粗口了,但这种文档真的会让你疯掉的!

你觉得这是一个很简单的需求,是吧?然后你就噼里啪啦的开始编码了,20分钟搞定,踌躇满志,小case啊!

然后噩梦就开始了。

第一次验收。发包方的眉毛皱了起来,“嗯,其实我想的是那种:当用户把用户名输入完成之后,能立即显示该用户名是否重复,而不用点击提交之后才显示这个结果,你看XXXX网站就是这样的”。OK,Ajax效果是吧?好呢,我改。感谢asp.net mvc,有现成的Remote实现,20分钟,收工,over!

第二次验收。发包方有点不耐烦了,“你这个还是不对啊!你看我给你说了XXXX网站,如果这个用户名不重复的话,要打个勾啊;错了,打个叉,再显示‘用户名重复’的提示啊……”。你有没有想哭的感觉?我忍,含着泪,找个勾勾叉叉的图片,再改。

第三次验收。发包方终于笑了,“不错不错!”你心里一块石头落了地。但这发包方思维很严密,他突然想到一个问题,“你说,假如两个用户同时注册,用的是同样的用户名,这个用户名也是以前数据库里没有的。所以页面输入的时候,肯定是显示用户名有效,是吧?所以他们都可以提交,但他们用户名又是一样的,都提交了就重复了,这时候会发生什么情况?……”你仔细的想了想,是不是觉得晴天霹雳天旋地转?

这只是一个非常非常简单普通的需求,都可以演化出无数种具体实现,更何况其他你可能从来没接触过的复杂需求?你作为一个程序员,只是觉得苦逼郁闷或者还得赶工加班;但对于公司来说,这就是个大麻烦了:工期延误、费用增加、信誉破产……这时候该追究谁的责任?我们可以设想这样的对话:

发包方向你们公司老大抱怨:“张总,你们这个项目都延期好几次了,我不好交代了呀!”

公司老大张总叫来项目经理:“这个项目怎么回事?赶快去催一下。”

项目经理找到你:“怎么一个功能做这么久?你是怎么搞的?”

你:“我怎么搞的?需求改了N遍了!我还想问你是怎么定的需求呢?你看……”

项目经理找到公司老大:“张总,这个客户的需求变了啊!一直改需求,我们……”

公司老大找到发包方:“这个刘总,这个项目你们改了需求啊!不光这个工期不行,费用也得考虑考虑啊,呵呵!”

发包方就炸了,“还要加钱?你们这些个奸商!合同不是写得好好的,一口价……”

“不是改了需求么?”

“我哪里改?就不过‘用户名不能重复’啊,我改成了‘用户名可以重复’?”

“不是。我们最开始以为……,结果……”

“你们为什么最开始不提出来呢?你们是专业人士,你们应该把这些问题考虑到的呀!还亏得我们思虑周全,我们业余人员都能想到的,你们怎么就想不到呢?你们怎么样的一个业务素质?……”

(程序员同学,说句题外话,老板在外面受的气不是你们所能想象的。你以为他们在外面吃喝玩乐好不潇洒,他们实际上就一个“三陪”而已,陪吃陪喝陪笑,不比做小姐的好到哪里去!都是给你们“背黑锅”“揩屁股”啊。)

就事论事,这件事的责任还真赖不到发包方。他的需求最多算是“不明确”,你不能说他“变更”了需求。但是,不明确的需求你接受了,责任就转移到你的身上了。退一万步讲,你是“专业人士”,你真的应该考虑得比发包方周全。

所以,你观察周围经验老到的程序员,他们拿到这个需求是不会立马动手敲代码的。他们一定会想一想,多问几个具体细节,觉得没有问题之后,才开始动手。所以他返工就少,他就是可以不加班,这就是人家的本事。

但是,正如我反复强调的,架构/管理的目的,应该是通过一种制度一种流程来避免上述问题,而不是靠个人技巧经验。追根溯源,解决这个问题,就应该“明确需求”——也就是说,需求不能这样懵懵懂懂大而化之,必须相当的明确清晰,具有唯一性。怎么实现?需求文档可测试化。或者你可以理解为:把需求文档像测试文档一样写。比如,上述需求就变成:

  • 在注册页面
  1. 输入用户名xxxx,移出焦点,页面不刷新,显示图标“叉”和错误提示:用户名重复
  2. 输入用户名yyyy,移出 焦点,页面不刷新,显示图标“勾”
  3. ……
  4. 这份需求/测试文档,交发包方审核确认;之后验收,就严格按该文档逐项进行,所有测试通过,验收合格。如果需要加需求,一般都得加测试;加测试,那不用说,我们就来谈谈时间费用的问题。

BuildDatabase

在一些文档规范严格的公司,实际上是同时有一份开发文档和测试文档的。但开发人员在整个开发过程中,并没有参考测试文档,所以最后就很容易造成测试阶段开发人员不断地返工,开发人员和测试人员之间矛盾尖锐。但最终80%的情况,还是开发的问题,毕竟你是做事的人,人家是检查的人。所以为什么不从一开始,就把需求文档、测试文档和开发文档合其为一呢?(当然,分开写,会有一些“监督”的作用,但我始终觉得,这种监督效率不高,投入产出不划算)

而且我发现,这些测试文档,都有一个问题:操作繁琐不经济。

仍以“用户名不能重复”为例,我看到的测试文档大致就是这样写的:

  • 以test-1112为用户名注册一个新用户
  • 再使用test-1112为用户名进行注册
  • 页面显示错误提示:用户名重复
  • ……

表面上看起来没有问题,但是如果测试次数多了的话,就会感觉每次先去注册一个新用户很麻烦。而且,这只是最简单的功能,如果功能复杂,要求准备的数据多/难/特殊,怎么办?我们又讲一个故事吧。

项目上线前夕,测试人手不够,我们开发过去帮忙。我跑一个test case,跑了一个多星期!你信不信?我的问题就在于测试的这个数据做不出来。测试文档是一步步写清楚了的,但你这样做不下去:权限不够、数据已有变化、文档模糊……到处找人问。找到懂这事的人,中途又发现,程序(页面)发生了更改,有些功能跑不起来……最后一个workaround,得改数据库,数据库又得找DBA啊……总之,这个test case搞得鸡飞狗跳,好在最后还是跑出来了——但以后(下一次)怎么办, 我就不知道了。

本质上,这种做法,测试数据是要测试人员自己“做”,或者“找”的,有很多问题。所以,从那时开始,我就在想:能不能把测试用的数据“固化”下来?让测试人员就基本上不用“做”,或者很方便的就能“找”打测试数据。比如:“用户名不能重复”的文档就直接写成:

  • 使用test-1112为用户名进行注册
  • 页面显示错误提示:用户名重复

最多加一个说明:test-1112 是已有用户名,用户名不能重复。

这个诱惑一直吸引着我,最后我在solution中就引入了一个BuildDatatase项目,专门为开发测试准备数据。毫无疑问,这个决定也遭到了开发人员的抵制。因为这个数据也不是那么好做的,具体我们将在项目详解里谈。

我铁腕推行,大概经过了两件事,他们慢慢的就习惯/认同了这种做法。

第一件事,是 任务列表页面。开发代码之前是写好了的,而且已经跑了一段时间了。我让他为该页面重新创建测试数据,他一脸不可置信:这种筛选排序,天?要准备多少数据?!怎么准备?代码都写好了,跑得好好的,有必要吗?

我让他先冷静一下。然后有以下对话:

“如果你不准备这些数据,你怎么保证你代码的正确性?”我问。

“就直接在页面上建几个任务啊,然后跑一下。”他想了想。

“要建几个呢?”我继续追问。

“……”,他一时答不出来。

“随便做几个数据,随便的跑一跑,然后就不管了,是吧?你以前就是这样做的?”,我接着说,“所以我们的代码没有质量。然后如果没有专门的测试呢,就让用户当免费测试员;有测试呢,我们就把这些脏活累活扔给他们。他们一遍遍的报bug跑不过,我们还不服气还有怨气……为什么我们不一开始就把它做好呢?”

首先确定要做,然后再想办法!很快我们就想到了一个办法,反过来做:满足所有筛选条件的任务就一条,然后逐个的减少筛选条件,每少一个筛选条件,就增加一个适格任务。这样理下来,共计25条数据就够了,并没有我们想象的多。从构思到文档再到最后“造”完所有任务,差不多花了一个下午的时间而已。最后,戏剧性的是:出效果了!跑一遍,我们立马发现了代码里面的两个bug。因为这种query查询写代码的时候都“复制粘贴”的,十几个条件,难免出错。如果不是这样跑一次,这些bug不知道什么时候能冒出来——因为我们自己用的筛选条件比较固定的,某些筛选条件从来没用过。所以我问他:你之前说“跑得好好的”?他只能呵呵了。

第二件事,是在任务编辑页面,我们要测试父子任务关系之间的自动化功能,这就需要比较复杂的一个“任务树”做开发测试数据,好在我们是做了的。代码review的时候,我在我这边一跑,不对,就直接打回去了;过了一会,他跑过来,代码没问题啊?当时我们脑子都短路,没想到其他,就先去看代码去了,折腾了好久。在他电脑上跑,然后又在我电脑上跑,又是设断点,又是看逻辑。

后来我突然灵光一闪,“是不是数据的问题?”

“嗯,有可能。但怎么确定呢?”

“你重新BuildDatabase再跑!”

果不其然,原来他自己跑的时候改动了数据,然后就忘了呀,一直就在已变动的数据上跑,当然没问题了。亏得我们有BuildDatabase,可以随时重建“基准”数据,否则,这种问题是相当花冤枉时间的!

这类似的情况其实很多很多,测试人员报bug之后,我估计开发人员最常用的一句台词就是:“我这里都能跑啊?!”所以问题80%都出在数据上——那为什么我们的数据就不能规范统一起来呢?

通过BuildDatabase,建立一个有序的可控的数据库,我们才能够在此基础上进行一系列的(测试驱动的)文档编写、开发和测试活动。所以说,BuildDatabase是“测试驱动”的基石和保障。除此以外,BuildDatabase还可以在项目发布、模拟展示等方面发挥重要作用。

(我以前公司的集成测试环境这些,数据库都是从生产环境中copy或截取的,我们需要的数据都是“自己造”,或者“自己找”的。这样做能基本满足开发测试需要,但中间总是很容易“出篓子”——正如我前面所说,一个test case可以跑一周。而且随着数据增加,这个copy也越来越难啊,一次导几百个G终究很累,所以好像是到了一定时候,还是得自行维护测试数据库——但维护主要是数据结构上的,比如增减列之类的,数据本身是无法维护的。

摘要:面向对象或者领域驱动,最重要一点就是要忘记数据库!我花了很长很长的时间,才理解了这一点,从而真正的迈向一个崭新的天地;而后,我又花了很长很长的时间,才勉强做到这一点;我希望,有一天这将不再是一个问题。

前面写了这么多,很大程度上就是为了这一章做准备。面向对象或者领域驱动,最重要的一点就是要忘记数据库!我花了很长很长的时间,才理解了这一点,从而真正的迈向一个崭新的天地;而后,我又花了很长很长的时间,才勉强做到这一点;我希望,有一天,这将不再是一个问题,我不需要考虑这一点……

为什么业务层这么薄

三层架构流行起来之后,我们很清楚的知道UI层负责页面交互并调用下一层,也知道DAL层就是和数据库打交道。但BLL层?什么才算是“业务逻辑”?有各种各样的解释,但这些不都是SQL做的么?对于绝大多数的应用系统而言,除了对数据库进行“增删改查”以外,实在不知道还能做些什么?更何况,不是还有超级强大的存储过程么!

所以,很多系统,即使勉强弄出一个业务层,也“薄”得不像话,像一层塑料薄膜一样,让人有一种把它立即撕掉的强烈的冲动。

为什么会是这样呢?

这得从.NET阵营从历史说起。.NET阵营的同学知道三层架构,多半是从PetShop开始,这被奉为三层架构的经典,很多项目甚至是直接复制其架构。在当时,它是一种了不起的进步。那时候,还是从ASP向ASP.NET转型的过程,很多SAP项目,SQL代码都还是写在HTML里面的!所以,UI和DAL的分离,无疑具有明细的示范效应。

但微软的步子,不大不小,刚好扯着蛋。

步子小一点,做成两层架构,估计一点问题都没有,大家都能接受;步子再大一点,就得上ORM,可惜微软当时还没条件支持。所以就搞出了这么个不明不白稀里糊涂的概念出来,折磨了我好久好久……

长期以来,.NET的阵营,在应用级层面,其实是“面向数据库”的。从DataSet、DataGrid、DataSourceBinder之类的,都可以看出来。即使是Entity Framework,最开始也是从数据库的表向.NET的类进行映射。这些,都极大的制约了.NET阵营同学面向对象的思维拓展。

好在我终于跳出来了。

面向数据库

为了说明,我们举一个最简单的例子。

需求是:记录文章(Article)的浏览数(ViewCount)。每当文章被阅读(View)一次,浏览数加一。

看到这个需求,你首先想到的是什么?是不是:

Update Article set ViewCount = ViewCount + 1;

如果是这样的话,恭喜你,你还牢牢的守住了“面向数据库”的阵地。

/*

面向数据库并不是不可接受的,面向对象也并不一定比面向数据库“高级”。
这只是两条道路的选择,如果你愿意看一看另外一条路的风景,就请继续;否则,就此打住吧。

*/

面向对象

public void View()
{
//从数据库中取出Article
Article article = session.Load<Article>(articleId);

//改变Article的ViewCount属性
article.ViewCount += 1;

//将改变后的Article再存入数据库
session.Save(article);
}

有什么感觉?眼前一亮,还是不可思议?想得更深一点的,是不是觉得这是多此一举,一句sql就能解决的问题,搞得这么复杂?

我当年,考虑最多的,最不能接受的,是 性能问题。

这必须利用ORM,即使不考虑ORM生成的sql高不高效,就这生成sql的开销,应该就不低吧?
这样做,取数据,打开一次数据库连接;存数据,又打开一次数据库连接。就算有连接池,但能省一点就省一点不是更好?
所以,如果你也和我一样,倒回去看我之前的博客吧!

这样做,还有其他很多具体的技术问题,我们后续博客会逐一展开说明。

为什么

我们还是回到大方向上来,为什么要这么做?换言之,“面向数据库”有什么问题,或者说“面向对象”有什么好处?

我觉得,“抽象”、“解耦”、“复用”之类的说法,都还没有触及根本。最根本的原因,还在于我们的大脑,我们的大脑不适应于把这个世界抽象成一张一张的表,而更适应于一个一个的对象。随着系统日趋复杂,这种现象就表现得越明显。

我曾经参与过一个项目,它的数据库结构打印出来,得用地图那么大一张纸(我不知道算A几了),密密麻麻的全是表,各种线条交错其中,我看着就头皮发麻。部门里面像个宝贝一样把这张表供着,因为公司没法打印也没法复印啊!(我不知道他们最开始是怎么得来的,估计肯定麻烦)

如果你一边读一边在想,就会发现,“不对呀,有多少表就有多少类,类图不是一样复杂吗?”

是的,而且由于抽象,类很可能比表还要多。但是,有于抽象,在我们进行架构、设计、沟通的时候,可以暂时的抛弃很多细节。比如,我们可以说,“文章 被评论之后,文章作者的积分加10分”,这个时候,我们就不考虑文章有很多种:博客、新闻、问答、评论……,也不考虑积分增加是直接改积分总分呢,还是添 加一条积分记录,或者还要同步……。如果只有表,你怎么说?

当然,表的结构也可以设计成类似于继承的样子(类的继承关系也最终会映射成表结构),但是,在交流沟通中,你如何表明这种抽象关系呢?

单纯从程序的角度上说,使用ORM,面向对象,还增加了系统的复杂性。毕竟多了一道工作,而且把对象映射到数据库不是一件简单的工作,尤其是你还要考虑性能问题的时候。

那为什么我们还要这样做?委托,换言之,把复杂性往其他地方推。我记得我反复讲过这一点,架构的一个重要工作,就是把复杂性进行拆分和推诿。拆分估计大家好理解,但“推诿”是个什么意思,推给谁呢?管它呢,我只做我分类的事,其他的,UI推给BLL,BLL推给DAL,DAL推给DBA,DBA推给采购部……

写在这里很搞笑,但事实就是这样的。在 性能篇,我说,你要写高性能的代码,你就是抢了人家的饭碗,就这个意思。UI都把DBA的活儿干了,人家吃什么?你代码都写成01001010101010二进制了,别说做汇编的,估计做CPU的都活不下去了。

我们这里,就是把复杂度推给了Map团队、ORM工具开发商和DBA。

因为我们要和客户谈需求啊,典型的是领域驱动,要和客户/领域专家找到“共同的语言”,这共同的语言是什么?是表结构?估计如果开发的是一个财务会 计系统,这还是可行的——估计早期的系统大多就是财务报表类系统?说不定还真是这样。为什么面向对象从Java开始流行,Java是虚拟机,可以用在微波 炉报警器之类上面的,底层数据结构可以完全脱离数据库啊!.NET做什么起家的,就报表啊!呵呵。

总之,发展到今天,随着系统复杂性的增加。在系统的架构设计中,我们不得不将现实世界首先映射成一个一个可以封装、具有继承多态特性的对象,并且将重心放在这些对象关系功能的维护上。

数据库?就先不管它吧。

只有脱离了数据库的束缚,我们才能自由的翱翔在面向对象的世界里!

忘不掉

“问题是我忘不掉啊!”

“我只要看到需求,脑子里马上就是数据库就是表。”

“没有数据库,我都不知道怎么开始写代码了。”

……

是的,忘掉数据库是很难很难——尤其是对于我们这些老人来说。已经浸淫sql数十年的高手,你让我忘掉它?你以为写小说啊,张无忌学太极啊?

我只能说说我是怎么做到的,希望能给你一些参考。

我就假设我的系统不是用“关系数据库”存储数据,不是mysql,不是oracle;我用nosql,我用xml文件存储,行不行?nosql,怎 么用?不知道啊,我十窍通了九窍。但我就要在我还不知道nosql怎么用的时候,就开始构建我的BLL/领域层。而且我只设定几个最简单的假设:

  • 所有的对象都可以直接从硬盘Load()出来
  • 所有的对象都可以直接Save()到硬盘
  • 对象之间用1:1、1:n、n:n建立关联即可

究竟怎么从硬盘里存取(所谓的“持久化”),以后再说。我连用什么进行持久化都不知道,现在怎么考虑?但有一条,反正不会用关系数据库,估计是用NoSQL吧……
最终的期望

真正的对象数据库!快出来啊,求你了……

惯例说我的项目进展:

1、写文档写到吐……

2、重构累成了狗……

本计划发布了新版本再写这篇博客的,但实在不能再拖了。博客系列接下来,就进入项目的具体开发了,代码还乱成一堆,啊……

   
1220 次浏览       34
相关文章

用户故事与用例
交互设计师之精益画布篇
数据分析之用户画像方法与实践
如何快速建立用户模型?
 
相关文档

用户界面设计
给企业做大数据精准用户画像
用户体验和交互设计
大数据下的用户画像
相关课程

用户体验&界面设计
用户体验、易用性测试与评估
用户研究与用户建模
用户体验的软件UI设计最佳实践
最新活动计划
嵌入式软件架构设计 12-11[北京]
LLM大模型与智能体开发实战 12-18[北京]
嵌入式软件测试 12-25[北京]
AI原生应用的微服务架构 1-9[北京]
AI大模型编写高质量代码 1-14[北京]
需求分析与管理 1-22[北京]

从手机登录页面设计想到的
如何把无意识引入交互设计中
交互设计的真相
当视觉设计师遇上产品经理
手机交互设计原则
用户体验之网页板块设计
更多...   

以用户为中心的设计
可用性评估
Desktop及Web-based视觉设计
认知原理与设计应用
手机用户界面设计

北京 以用户为中心的界面设计
北京 用户体验& 界面设计
上海 华为 用户体验& 界面设计
深圳 用户体验& 界面设计
爱立信 以用户为中心的设计
北京 用户体验与界面设计
福州 以用户为中心的界面设计
更多...