在这一篇中,我们将采用Step By Step的方式,重点介绍如何在实际开发过程中,使用测试先行、代码复查和重构。形成这样的开发习惯之后,编写代码将不再是实现、排错的过程,而变成制造快乐的过程:写一个测试、实现、测试通过,再写测试、再实现、再通过测试。完成一件任务,能引发多处阶段性的快乐,这对于我们的工作心态大有好处。
我们的工作目标,是为Db类增加三个构造方法。
先解释一下为什么要先写测试,再写代码,实际上,有下面的七个方面的好处:
1、关注点集中
2、单一的成功被无数成功取代,心态将更为良好:脑力工作,情绪是非常重要的,大家如果正常的统计自己的有效工作时间,往往会发现自己一天的有效工作状态持续时间非常短。一个很经典的说法,是多数程序员每天正常工作的时间平均下来不超过一个小时。
3、占在类的用户角度,来看待类的设计,有助于
4、建立起整个项目的质量基础
5、累积下来的单元测试,是后期代码修改的准则
6、Bug的修复将更为精准。
7、单元测试,就是团队中其他程序员的使用手册。
一鱼七吃,这样的事情,为什么不做?
代码复查,简单的说就是逐行阅读代码,从而找出潜在的问题和代码结构上的坏味道。据说,代码复查能够发现项目中一半以上的Bug,当然,在代码尚未发布、或者Bug尚未转移到团队其他成员处的时候,修复Bug的成本是最低的。
代码复查是很枯燥的工作,自己复查、团队成员交互复查都行。敏捷方法中比较极端的例子是双人编程,这样,实际上代码复查贯穿于整个开发时间。
重构,消除代码中坏的味道,这里最常见的一种现象是“重复的代码”。
由于有大量的单元测试,重构过程中保持全部单元测试通过,有助于实现我们的目标:仅仅调整代码的结构,而不会令功能实现上出现问题。
代码质量,并不是代码工作如何精准,而是代码的结构是否做到了最简化。请注意,稍稍不留意,代码增加的少许复杂性,会令同事阅读困难、甚至令自己阅读困难,各处的复杂性累积之后,会造成Bug数量的大幅增长,维护也变得困难。这里可以参考用户体验的一句名言“多一次击键,用户可能永远不会使用这项功能”。
唠叨完毕,我们先做好准备。
我们创建一个单元测试项目,引用我们的工作项目Faster.Data类库项目。在单元测试项目中增加一个配置文件App.config,我们将数据库连接信息保存在这个配置文件中。
单元测试中最常见的问题,是“数据库如何测试?”
我曾经饶有兴趣的使用Mock,但最终对自己定下了规矩,即今后绝不使用任何Mock。原因:Mock对象会大幅增加单元测试的简洁性,构建Mock对象有时候也需要大量的工作,充满了Mock的单元测试可读性很差,团队成员需要全体掌握相关的知识从而导致对开发人员的门槛提高,这也意味着成本的提高。
不过,在这个类库创建完毕后,数据库读写操作就有了很好的基础,今后所有应用项目则根本无需对数据库读写进行单元测试。
我的方法是,建立测试数据库,保证数据库所有表格处于空白的无记录状态,即针对每一项测试,测试前要准备数据,测试后毁尸灭迹。
那么,现在开始为Db增加构造方法,我们先做最原始的:
第一步 在类图中为Db添加一个构造方法 Db(string connectionString,string providerName)
为什么先做这个?因为这是与配置文件无关的,Db(string configName),这个从配置文件中获取上述两个参数,Db()则默认的使用ApplicationServices配置项获取上述两个参数。我们显然要先做最简单的。换句话说:先解决构造问题,再解决配置文件的问题。
第二步,切换到这个构造方法的代码,右键,选择创建单元测试。此时,Ide帮我们在下是项目中增加了一个代码文件,测试类的名称为DbTest,嗯,类名加上Test。
那么,这是第一个基本的概念,测试是以类为单位的,每一个待测试的类,对应一个测试类。多数情况下,类的每一个public成员,对应一个测试方法。我们要牢记两点:1、不针对私有成员写测试;2、每一个测试方法都要足够的简单,突出该方法的目的。
第三步,我们在测试方法中,写上这样的代码
Db db= new ("连接字符串","提供者名称");
Assert.IsNotNull(db); //我们期望上一行创建的db对象不为空
第四步,运行测试,出现绿色的“成功”提示。好,现在这个测试已经通过。
第五步,现在我们要确定Db已经正常的连接到数据库。对,如您所说,Db的State状态必须为Open
我们在测试方法下,再增加一行
Assert.AreEqual(ConnectionState.Open,Db.State);//期望创建的连接已经打开
运行测试,当然失败了,因为我们还没有做真正的连接工作。
第六步,我们为刚刚写的Db的构造方法中增加如下的代码:
factory = DbProviderFactories.GetFactory(provider);
connection = factory.CreateConnection();
connection.ConnectionString =connectionString;
connection.Open();
第七步,运行测试,通过。嗯,现在这项工作完成。
按照同样的方式,创建其他两个构造方法。比如:
第八步:从某个配置项中定义的连接字符串,创建连接
var setting = ConfigurationManager.ConnectionStrings[config];
factory = DbProviderFactories.GetFactory(provider);
connection = factory.CreateConnection();
connection.ConnectionString =connectionString;
connection.Open();
第九步 重构
我们简单的审阅代码,会发现第六步和第八步的代码,显然有多数重复。那么,创建一个私有方法CreateConnection,包含如下代码:
factory = DbProviderFactories.GetFactory(provider);connection = factory.CreateConnection();
connection.ConnectionString =connectionString;
然后,第六步的代码便是:
CreateConnection(connectionString, provider);
第八步的代码变为:
var setting = ConfigurationManager.ConnectionStrings[config];
CreateConnection(setting.ConnectionString, setting.ProviderName);
重复,是最常见的“坏味道”,这里是最浅显的例子。
然后,发现这种从抽象类DbConnection继承,然后内置一个DbConnection对象,将所有操作转移到该对象的方法,实现起来颇多不自然之处。包括:
1、我们实现了所有的抽象成员,但GetScheme之类的成员没有实现。
2、内置的protected只读属性DbProviderFactory没有实现
3、这个对象实际上包括两个DbConnection的实例,一个是它自身,一个是内置的connection实例,思路上比较紊乱。
此时面临着问题:是采用静态类的方法,还是这种创建自定义Connection的方法,于是我们在团队项目中增加一个问题Impediments
在项目开发过程中往往面临着各种问题,有的是设计上的决策,有的是技术难题。每个成员当遇到这类影响进度的问题时,需要提交相应的信息,至迟在第二天早上的15分钟例会中,项目经理和团队其他成员应能够注意到,并优先解决这些问题。问题的优先级大于任务。
比如,这里如果要更改设计,则面临着前面的单元测试要重写、这个类的所有代码要重新组织。
此时,我们最终选择改用静态类的方式去做。这样,我们关闭这个问题,因为我们已经做出了决策。
增加一个任务:重构,Db改用静态类方式,不再从DbConnection继承。
经过半小时的工作,这个任务完成,新写的4个单元测试正常运行通过。由此,我们知道,由于有单元测试基础,这种更改设计的决策,比较容易下决心。
如此,我们通过创建4个单元测试、从而创建三个构造方法,完成了这一组任务。过程中,根据进度,在Tfs团队项目中将各项task的状态分别由in progress 改为done。
看看,比较满意吧?运行所有测试,通过。相关的任务,done。将代码签入到源代码服务器,接着做剩下的其他工作…