⾯向测试编程--代码的可测性
背景
这是之前参加的⼀个⼯程师交流会上别⼈分享的⼀个⼩议题,做了⼀些笔记,后⾯整理资料的时候⼜从⽹上搜集了⼀些做补充,今天分享⼀下代码的可测性
测试性不好的代码特征
缺陷1: 构造函数做了实际⼯作
构造函数或域声明中出现new字眼
构造函数或域声明中调⽤静态⽅法
构造函数做了分配域字段之外的事情
构造函数中,对象初始化⼯作没有完成彻底(⼩⼼初始化⽅法)
构造函数中,出现了控制流(基于条件或循环的逻辑)
在构造函数内构造复杂的对象图,⽽不是使⽤⼯⼚(factory)模式或构造器(builder)模式
增加或使⽤初始化块
缺陷2: 滥⽤协作类
引⼊了对象,却不直接使⽤(⽽是⽤于通往其它对象)
不遵守迪⽶特法则:对象图中,⽅法引⽤链包含了多个下标点(.)
可疑命名:如context,environment,principal,container或manager
缺陷3: 脆弱的全局变量&单例(Singleton)
增加或使⽤单例
增加或使⽤静态域字段或静态⽅法
增加或使⽤静态初始化块
增加或使⽤寄存器
增加或使⽤服务定位器
缺陷4: 类做事太多
总结该类的作⽤时,得使⽤以及之类的描述语
团队新成员很难读懂和快速接⼿该类
该类的某些域字段只⽤到部分⽅法中
该类的某些静态⽅法仅针对参数操作
构造函数
⼀个常见的测试性不好的地⽅就是在构造函数中做了实际⼯作,在构造函数中执⾏功能相当于让对象实例化难上加难,或者说给对象模拟增加困难
例1: 构造函数或域声明中出现了new字眼
class House {
House() {
kitchen = new Kitchen();
bedroom = new Bedroom();
}
}
上⾯代码的问题:
混杂了对象图的创建与功能逻辑,对象图是指创建不同的实例对象,如何在这些对象之间进⾏逻辑处理则是独⽴的
上⾯的代码或许容易实例化,但如果Kitchen代表的是某类⾼成本事项,如⽂件/数据库存取等,那么它就不太易测,因为难以使⽤模拟对象类替代Kitchen或Bedroom
设计很脆弱,因为不能将House⾥的kitchen或bedroom的⾏为以多态的⽅式进⾏替换
良好的代码风格是不要在构造函数中创建协作对象,⽽是将已创建的对象作为参数传递进去
class House {厉害的编程代码
House(Kitchen &k, Bedroom &b) {
kitchen = k;
bedroom = b;
}
}
在随后测试时,可以先在外围创建协作对象,然后在创建House对象时,通过参数传⼊
例2: 构造函数拿到⼀个部分初始化的对象,⽽且必须设置它
class Garden {
Garden(Gardener joe) {
joe.setWorkday(new TwelveHourWorkday());
joe.setBoots(new BootsWithMassiveStaticInitBlock());
this.joe = joe;
}
}
建⽴对象(为Garden创建和配置协作对象Gardener)的⼯作不应当由Garden来做,若配置和实例化混杂在构造函数中,对象就变得更不友好,并且与特定的实体对象图结构绑定,这样就使得代码难以修改,⽽且不易测
Garden需要Gardner,但配置garnder不是Garden的职责
Garden的workday在构造函数中被特别设定了,因此迫使让Joe每个workday⼯作12⼩时,像这样的强制依赖是不友好的
良好的代码风格是把已经完全初始化的协作对象传进需要的类中
class Garden {
Garden(Gardener joe) {
this.joe = joe;
}
}
例3: 构造函数违反迪⽶特法则(⼀个实体要尽可能的只与和它最近的实体进⾏通讯)
class AccountView {
User user;
AccountView() {
user = Instance().getUser();
}
}
上⾯的代码只需要User对象,但是却访问了全局作⽤域中的RPCClient的单例(singleton)
上⾯的代码不仅在构造函数中做了实际⼯作,破坏了静态⽅法的⽆缝性,⽽且违反了迪⽶特法则
为了测试上⾯的代码,就必须创建⼀个RPCClient实例(静态⽅法不可避免,也⽆法模拟),但是被测试类不需要RPCClient,只需要User,但是为了测试,就必须做这些额外的环境布置,导致可测性不好
所有需要构造AccountView类的测试都必须处理这些问题,⽐如AccountServlet可能需要AccountView,⽽在测试AccountServlet
时,RPCClient实例的预布置⼯作都要做
良好的代码风格是只传⼊直接需要的东西,⽐如协作对象User,这样在测试时,只需要创建真实的或模拟的User对象,这种设计更加灵活⽽且更具可测性
class AccountView {
User user;
AccountView(User user) {
this.user = user;
}
}
User getUser(RPCClient rpcClient) {
User();
}
RPCClient getRPCClient() {
return new RPCClient();
}
例4: 在构造函数中创建并不需要的第三⽅对象
class Car {
Engine engine;
Car(File file) {
String model = readEngineModel(file);
engine = new EngineFactory().create(model);
}
}
上⾯代码为了制造⾃⼰的引擎engine,要求⼀辆汽车Car去获取⼀个引擎⼯⼚EngineFactory,这是难以理解的,汽车得到的应该是造好的引擎,⽽不是去弄清如何制造引擎
构造函数并不需要直接访问第三⽅对象,⽽只是需要第三⽅对象创建的东西
上⾯代码在构造函数中创建第三⽅对象,⽆法注⼊也⽆法覆盖,但却致使代码更加脆弱,因为⽆法换掉factory对象,也不能试图缓存它,⽽且⼀旦新的Car对象被创建,就⽆法阻⽌第三⽅对象的继续运⾏
所有需要构造Car类的测试都必须处理这个问题,⽐如有个Garage的测试需要Car,为了顺利调⽤到Car的构造函数,就不得不去创建新的EngineFactory
良好的代码风格是去掉第三⽅对象,⽤简单的变量赋值取代它们在构造函数中的⼯作,将预先配置好的
变量加⼊到构造函数的成员变量中,让别的对象(factory,builder或Guice Provider)为构造函数参数完成构造⼯作; 将主体对象从对象图构建职责中解脱出来,可得到⼀个更灵活,维护性更好的设计
class Car {
Engien engine;
Car(Engien engine) {
}
}
Engien getEngien(EngineFactory engineFactory, String model) {
ate(model);
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论