项目起源
在开发中我们需要观察数据Model是否正常的显示在UI中,在单元测试的负责显示的View层需要检测的是页面是否正常显示。它们本质其实就是验证ViewObject(VO)表现层对象的正常表现。
在一般的开发我们怎样模拟VO,手动生成?
UserModel userModel=new UserModel("userId","userName","用户等级100");
像这样,然后用这个model传入到View层显示起来,然后检测正常显示。
这样的方式有几个问题:
- Model的内部字段如果很多,构造参数就会很长,使用不方便。
- Model的内部结构变化,新增或删除字段都会影响构造参数,就会影响其测试模块构造VO部分的代码。
在早期的开发中,如果界面结构还在调整中VO的结构经常会发生改变。在产品的迭代中产品新的界面形式也会导致VO结构的修改,这些都是常常发生的。如果每次修改都导致我们的VO的模拟代码变化,就是很痛苦的事情了。
分析问题
我们来看看典型的Model结构,比如用户相关的Model
public class UserModel {
private String userId;
private String username;
private String phone;
private int age;
private float grade;
}
仔细想想这些字段所描述的信息,比如:
- String userId:字符串类型的用户id,一般由数字和字母构成
- String phone:字符串类型的用户手机号,一般由11位数字构成
- int age:整数类型的用户年龄,一般数值范围在1~120的整数
所以我们可以了解到,本质上VO就是具有特定意义的字段类型和字段名构成的。 只要确定好类型和名称间的关系,字段的值的范围我们是可以确定的。
所以关于模拟VO就变成了处理特定的字段类型和字段名的规则,然后得到一定范围内的数据。
代码实现
理解了前文的意思,我们的最开始的模拟VO数据就变成了
UserModel userModel=Virtual(类型信息,数据规则).build(应用信息和规则生成数据)
这是最简化的模拟表示,我们要进一步完善变成实际代码。
类型信息
如何得到类型信息,这里最容易联想到的就是反射。通过外部传入的确定好的类.class然后反射得到它全部的字段(域field)信息。
Field[] fields = tClass.getDeclaredFields();//获取该类声明的全部域
数据规则
数据规则就是我们根据一般业务的理解后整理出来的特定数据类型和字段名确定的数据规则。然后把这些规则整理成代码。 如:
Map<String, RandomInterface<String>> map
map.put("name", new RandomStringChinese(6));//name的String类型,就是6个长度的中文字符
实例化
有了.class类实例化就是得到某个构造方法后,调起实例化方法,最后返回实例。
Object object = constructor.newInstance();
return (T) object;
这里需要强调目前不能直接从Constructor构造类中得到有效信息,只能使用空参构造方法得到空数据的对象,然后一个个填满field域。所以有一点小限制。
关于代码的设计模式
我们面对的是构建复杂对象,并且需要提供默认配置和外部设置,共同作用最后构建出Model对象。这里很容易就联想到Builder构造者模式(生成器模式)。
生成器模式的本质:分离整体构建算法和部件构造
分离构建算法和部件构造,从而使得代码结构更松散,更容易扩展,复用性更好,同时也会使得代码结构清晰,意图明确。
这里定义接口,并提供默认实现。
/**
* Builder接口,定义数据规则部件的结构
*/
public interface VirtualDataBuilder {
//定义各种数据类型的规则结构
}
/**
* 实现Builder接口,提供默认规则
*/
public class VirtualDataDefaultBuilder implements VirtualDataBuilder {
//实现各种数据类型的默认数据规则
}
同时在构造UserModel对象和List<UserModel>时,虽然都是构造有关UserModel的数据,但是实际部件不一样,就需要区分出:build()和buildList
使用
前面说了这么多的,最后组件完成,使用起来就非常简单。
UserModel userModel = VirtualData.virtual(UserModel.class)
.build();
List<CommodityModel> userModels = VirtualData.virtual(CommodityModel.class)
.buildList();
CommodityModel commodityModel = VirtualData
.virtual(CommodityModel.class, new MyVirtualDataBuilder())
.build();
使用说明
我整理出的数据类型和规则已经可以应付大多数数据类型,即使不够也提供了外部规则的设置,但是有一些特别的还是需要说明的。
有具体意义的Model
在我的项目中有一些特殊的Model或者是VO,它虽然和普通的VO看起来一样,但是在使用时不是直接使用,而是需要经过转换后使用。 如
public class ImageModel {
public String id;
public String hash;
}
它id的hash对应服务器上的图片资源,两者组合加上服务器地址构成实际的图片地址。这类的VO依靠的普通的数据规则可以生成非空数据,但不是有实际意义的数据。 如果需要实际意义的数据就需要在特定字段名出现时,直接返回有特定意义的Model数据而不是模拟数据。
public class MyVirtualDataBuilder extends VirtualDataDefaultBuilder {
/**
* 注入Object类型即 自定义的model数据规则
* 实现全新的定义规则
*/
@Override
public Map<String, RandomInterface<Object>> injectRuleModel(
Map<String, RandomInterface<Object>> map) {
//针对一些有特殊含义的Model,里面的字段有具体含义 不能根据类型模拟 只有针对特殊的字段名做数据处理
map.put("imageModel", new RandomInterface<Object>() {
@Override
public Object getRandomData() {
return new ImageModel("特殊Model的id有具体的含义,无需模拟", "特殊Model的hash有具体含义,无需模拟");
}
});
map.put("avatar", new RandomInterface<Object>() {
@Override
public Object getRandomData() {
return new ImageModel("用户的头像图片id", "用户的头像图片hash");
}
});
//直接返回 入参
return map;
}
}
不明确的数据类型
普通的VO一般就是Class:字段名或者是List<Class>:字段名这样一对一的关系。但是在Map这样的Key-Value多个类型的数据中,无法明确数据类型。所以只能返回null空或者只根据字段名返回特定数据。
//Map结构因为包含两个类型 无法直接使用数据命名规则匹配 只有特殊字段名直接赋值
map.put("userMap", new RandomInterface<Object>() {
@Override
public Object getRandomData() {
Map<String, Integer> hashMap = new LinkedHashMap<>();
hashMap.put("因为map结构有两个类型,无法统一处理,只能直接返回", 100);
return hashMap;
}
});
最后
这里说明的使用反射+数据规则快速构建Model的过程,具体代码已经开源 项目地址走过路过点个star, 开源库已经发布到jcenter,集成到项目只需要
implementation 'com.licola:virtual:1.2.0'
最后的效果就是:一行代码模拟出有效数据,方便测试和开发,即使Model的结构变化也不会影响现有模拟代码。
UserModel userModel = VirtualData.virtual(UserModel.class)
.build();
灵感来源
来自一篇文章的提示
利用JAVA反射机制获取所述数据对象的属性信息;根据所述数据对象的类型和属性信息,按照设定的规则生成用于进行插入测试或/和更新测试的测试用数据 ---测试用数据的生成方法、单元测试方法以及单元测试系统。