最近遇到一个单元测试的问题,本周正好学个了一个SCORE法则,这里正好练练手应用此法则将问题的前因后果分享给大家。
S:背景
代码要有单元测试,检测的标准就是统计代码的单元测试覆盖率,程序员需要达到指定的最低覆盖率要求。
C:冲突,或者叫问题吧
项目结构与代码扫描工具的特殊关系导致需要额外写更多的单元测试,因为目前开发管理部门的代码描述配置的是按JAVA工程来扫描,并不能将多个工程当成一个整体来扫描。
我的一个项目将接口以及实体对象单独成立为一个JAVA工程,整个项目分成两个JAVA工程:
- 接口以及实体,工程名称为core
- 业务逻辑以及数据持久化的实现,工程名称为service,依赖上面的core
一般情况下,由于core里面只包含接口以及实体,所以我没有意识到去写单元测试,因为单元测试的价值会比较小,无非就是测试实体是否可以序列化,如果实现了JSR303,那么这些校验的逻辑可能也有点测试的价值。由于我们的service依赖core,在为service写单元测试时,实际上已经调用了接口以及实体,理论上是不需要再为core去写单元测试的。但核心问题时代码扫描工具目前开发管理部门做的还没这么智能,它是以单个JAVA工程来统计单元测试覆盖率的,针对我们的结构如果只在service中写单元测试,那么有效的代码覆盖行只会统计service项目中的,至于调用的core项目中的代码并不包含在其中。而core的这些接口以及实体所占的代码行还是有一定分量的,如果不将这些统计进来那么想达到高的覆盖率还是比较费劲的,除非你有大把的时间去写。
O:选择的方案
实体对象无非就是一些get,set成本的方法,要想测试它们我们可以利用序列化机制,对象序列化成字符串会完成get调用,反过来将字符串序列化成对象会完成set的调用,那如何实现呢?
- 为每个实体对象,编写单元测试,实例化对象,最后完成序列化与反序列化。
优点:可以精确的控制每个属性的值
缺点:需要编写众多单元测试,时间成本高,且新增加实体类就意味着要编写新的单元测试,删除或者修改也会影响。
- 利用反射机制,动态计算工程中的实体,自动去完成序列化与反序列化。
优点:省事,只需要少量代码即可完成所有实体类的单元测试工作,且不会因为新增加实体量而编写单元测试
缺点:不能精确控制实体中的特定属性的赋值,但如果有特殊案例,可再单独编写单元测试来补充。
- 优化代码扫描工具
理论上是可行的,但有难度,而且也不灵活,工具是死的只会按照事先写好的规则去执行,比如现在的状况就是它只负责按单个JAVA工程去扫描。
R:结果
从笔记的标题可以看出来,我肯定是选择了方案2这种偷懒的做法,针对这类实体类的测试做到了不随实体类的增加与减少而去变更单元测试用例,节省出来的时间价值太诱人。
E:评价,这里因为只是我个人使用,所以属于个人的一些总结吧
在需要满足公司的代码规矩的时候,需要注意自己的实现方法,尽量提高效率,偷懒才会更加放松愉快的工作。
实现过程:输入一个包含实体类的包命名空间,系统加载包下面所有类,如果是枚举调用枚举方法,如果是非枚举生成默认实例对象并完成序列化与反序列化。
- 按指定的package加载类,传递一个包的命名空间,返回此包下面所有类。此段代码是借鉴网上的,据说这是spring源码中的一部分,具体我还没有核实。
public static Set<Class<?>> getClasses(String pack) { Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
boolean recursive = true;
String packageName = pack;
String packageDirName = packageName.replace('.', '/');
Enumeration<URL> dirs;
try {
dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
while (dirs.hasMoreElements()) {
URL url = dirs.nextElement();
String protocol = url.getProtocol();
if ("file".equals(protocol)) {
String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
findAndAddClassesInPackageByFile(packageName, filePath,
recursive, classes);
}
}
} catch (IOException e) {
e.printStackTrace();
} return classes;
} public static void findAndAddClassesInPackageByFile(String packageName, String packagePath, final boolean recursive, Set<Class<?>> classes) {
File dir = new File(packagePath);
if (!dir.exists() || !dir.isDirectory()) {
return;
}
File[] dirfiles = dir.listFiles(new FileFilter() {
public boolean accept(File file) {
return (recursive && file.isDirectory())
|| (file.getName().endsWith(".class"));
}
});
for (File file : dirfiles) {
if (file.isDirectory()) {
findAndAddClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive, classes);
} else {
String className = file.getName().substring(0,file.getName().length() - 6);
try {
classes.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
- 循环加载的类来做处理。因为实体对象中包含有枚举,枚举因为我们有自己固定的规则所以需要区别对待。来看看枚举的定义:
包含两个无参的实例方法与两个有参的静态方法,且继承了一个接口IEnumCodeName
public enum AppOwnerType implements IEnumCodeName { Enterprise(1, "Enterprise"),
User(2, "User"); private String name;
private int code; private AppOwnerType(int code, String name) {
this.name = name;
this.code = code;
} public static AppOwnerType getByCode(int code) {
return EnumHelper.getByCode(AppOwnerType.class, code);
} public static AppOwnerType getByName(String name) {
return EnumHelper.getByName(AppOwnerType.class, name);
} public String getName() {
return name;
} @Override
public int getCode() {
return code;
} public static void main(String a[]){
System.out.println(AppOwnerType.Enterprise.getName());
}
}
判断当前类为是否是上面我们定义的枚举,通过是否实现IEnumCodeName接口为依据。这里可以看出来在项目中为枚举定义一个接口是多么的重要
private boolean isEnumCodeNameByObj(Class<?> classObj){
Class<?>[] interfaces=classObj.getInterfaces();
if(null==interfaces||interfaces.length==0){
return false;
}
List<Class<?>> interfaceList=Lists.newArrayList(interfaces);
Object enumCodeNameObj=Iterables.find(interfaceList, new Predicate<Class<?>>() {
@Override
public boolean apply(Class<?> input) {
return input.getName().indexOf("IEnumCodeName")!=-1;
}
},null);
return null!=enumCodeNameObj;
}
- 如果类为枚举,执行枚举方法的测试。
private void testEnum(Class<?> classObj) throws Exception {
EnumHelper.IEnumCodeName enumCodeName=ClassloadHelper.getFirstEnumByClass(classObj);
Method[] methods= classObj.getMethods();
if(null!=enumCodeName) {
Method methodCode = classObj.getMethod("getByCode",new Class[]{int.class});
methodCode.invoke(null,enumCodeName.getCode());
Method methodName = classObj.getMethod("getByName",new Class[]{String.class});
methodName.invoke(null,enumCodeName.getName());
} }
- 如果类是非枚举,生成默认的实例然后再调用序列化与反序列化。(JsonHelper是封装的jackson,这里就不贴了)
private void testObj(Class<?> classObj) throws Exception { Object obj = classObj.newInstance();
String jsonString = JsonHelper.toJsonString(obj);
Object objNew = JsonHelper.json2Object(jsonString,classObj);
Assert.isTrue(null!=objNew);
Assert.isTrue(!StringUtils.isBlank(jsonString));
}
- 单元测试代码:
@Test
public void testPojo() throws Exception {
Set<Class<?>> classes=ClassloadHelper.getClasses("xxx.core.model");
if(null!=classes){
for(Class classObj:classes){
try {
boolean isEnumCodeName=this.isEnumCodeNameByObj(classObj);
if(isEnumCodeName) {
this.testEnum(classObj);
}
else {
this.testObj(classObj);
}
}
catch (Exception e){
e.printStackTrace();
}
}
}
}