最近遇到一個單元測試的問題,本周正好學個了一個SCORE法則,這裡正好練練手應用此法則將問題的前因後果分享給大家。
S:背景
代碼要有單元測試,檢測的標准就是統計代碼的單元測試覆蓋率,程序員需要達到指定的最低覆蓋率要求。
C:沖突,或者叫問題吧
項目結構與代碼掃描工具的特殊關系導致需要額外寫更多的單元測試,因為目前開發管理部門的代碼描述配置的是按JAVA工程來掃描,並不能將多個工程當成一個整體來掃描。
我的一個項目將接口以及實體對象單獨成立為一個JAVA工程,整個項目分成兩個JAVA工程:
一般情況下,由於core裡面只包含接口以及實體,所以我沒有意識到去寫單元測試,因為單元測試的價值會比較小,無非就是測試實體是否可以序列化,如果實現了JSR303,那麼這些校驗的邏輯可能也有點測試的價值。由於我們的service依賴core,在為service寫單元測試時,實際上已經調用了接口以及實體,理論上是不需要再為core去寫單元測試的。但核心問題時代碼掃描工具目前開發管理部門做的還沒這麼智能,它是以單個JAVA工程來統計單元測試覆蓋率的,針對我們的結構如果只在service中寫單元測試,那麼有效的代碼覆蓋行只會統計service項目中的,至於調用的core項目中的代碼並不包含在其中。而core的這些接口以及實體所占的代碼行還是有一定分量的,如果不將這些統計進來那麼想達到高的覆蓋率還是比較費勁的,除非你有大把的時間去寫。
O:選擇的方案
實體對象無非就是一些get,set成本的方法,要想測試它們我們可以利用序列化機制,對象序列化成字符串會完成get調用,反過來將字符串序列化成對象會完成set的調用,那如何實現呢?
優點:可以精確的控制每個屬性的值
缺點:需要編寫眾多單元測試,時間成本高,且新增加實體類就意味著要編寫新的單元測試,刪除或者修改也會影響。
優點:省事,只需要少量代碼即可完成所有實體類的單元測試工作,且不會因為新增加實體量而編寫單元測試
缺點:不能精確控制實體中的特定屬性的賦值,但如果有特殊案例,可再單獨編寫單元測試來補充。
理論上是可行的,但有難度,而且也不靈活,工具是死的只會按照事先寫好的規則去執行,比如現在的狀況就是它只負責按單個JAVA工程去掃描。
R:結果
從筆記的標題可以看出來,我肯定是選擇了方案2這種偷懶的做法,針對這類實體類的測試做到了不隨實體類的增加與減少而去變更單元測試用例,節省出來的時間價值太誘人。
E:評價,這裡因為只是我個人使用,所以屬於個人的一些總結吧
在需要滿足公司的代碼規矩的時候,需要注意自己的實現方法,盡量提高效率,偷懶才會更加放松愉快的工作。
實現過程:輸入一個包含實體類的包命名空間,系統加載包下面所有類,如果是枚舉調用枚舉方法,如果是非枚舉生成默認實例對象並完成序列化與反序列化。
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()); } }
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(); } } } }