Pretty工具类:让软件开发调试与单元测试更happy!
来源:互联网 发布:淘宝众筹靠谱吗 编辑:程序博客网 时间:2024/06/07 10:25
在软件开发调试过程中,经常会去查看某一对象的取值。但类之间复杂的层次关系,再加上数组(链表)、映射(字典)等多种数据结构,让我们难以一目了然。本文介绍的Pretty工具类,以缩进的方式突出类之间的层次关系,并且将对象一层层的整个结构pretty地打印出来!
在编写单元测试时,经常会去比较某一对象是否符合预先的期望值。但对于一个复杂类的对象,这种单元测试并不好写,容易片面化、复杂化。Pretty工具类,既能够完整的检测复杂类的对象,而且可读性好,便于理解代码。当检测出与期望结构不匹配时,不仅可以输出Diff信息,还能提醒用户是否需要自动更新case,简单易用。
本文实现了3种语言版本的Pretty工具类:Java版,Python版,Groovy版。这里对Java版的Pretty做了重点介绍,而其它版本只是简单带过,因为实现的原理都是一样的,只是换成不同语言而已。最后,对于同样广泛应用的C++,本文虽然没有给出具体实现,但也提供了一个设计思路,有兴趣的朋友可以试一试。
1. Pretty之Java版
1.1 调试中的问题
当我们在调试程序的时候,经常会查看某一变量的值。一般来说,有两种方法被经常用到:
1. 用调试器,如Eclipse Debug,或者gdb/pdb。
2. 用print函数或者logger,直接将变量值打印出来。
这两种办法都有缺点,调试器需要一层层展开看,而且如果杯具碰到链表结构或者哈希表的时候,就不太容易看明白了。而print函数其实只是toString方法的返回值,取决于toString函数的实现,其实并不可靠。
可能有聪明的读者会想到,那我们在定义类的时候,都override一下toString方法,让它可读,而不是Object类的缺省实现(JVM中的地址)。
这是一个很天真的想法:
1. 首先,不是所有的类都能够由我们控制,如Java类库,第三方库,其他开发团队的代码,等等。
2. 类的toString方法可能有它的业务价值,而不只是为了方便调试。
3. 额外工作量:这不是必须的,若是其中要求每个类都去override toString方法,会增加很多没有必要的工作量,产生很多不必要的代码,反而会增大维护代码的工作量,甚至引起bug。而且,当类每次增加/修改/删除成员变量时,都要去修改toString方法,否则print出来的信息也就不可靠了,但这是很难保证的。
1.2 单元测试中的问题
有下面一个类A,聚合了类B,而B又聚合类C,如下代码:
class A { private int id; private File path; private Integer[] array; private List<String> list; private B b; // ... } class B { private String desc; private Map<String, Date> map; private C c = new C(); // ... } class C { private double v1; private BigDecimal v2; // ... }
现在我们先测试一下类 A 的对象 a 是不是所期待的,一般容易想到下面几个方法:
1. 把所有成员变量都 get 出来比较:
assertEquals(xxx, a.getId());assertEquals(xxx, a.getPath());...assertEquals(xxx, a.getB().getDesc());...assertEquals(xxx, a.getB().getC().getV1());...
这种办法的问题显而易见:如果不小心漏了一个重要成员变量的get,那测试就不够全面了。而且并不是所有成员变量都有get方法,需不需要get方法得看具体需要,而不能只为unit test专门提供。而且,像这么简单的类,都需要这么多行assertEqual,如果有更多的成员变量或者有很深的聚合层次,那将无法想象。如果A类的结够在做个调整,那改动的地方就很多了。这样的unit test维护成本之高可想而知,还有谁有动力写unit test,因为那是在给自己找麻烦!而且,不是所有的代码我们都能控制的,比如第三方库。
2. 为A类实现equals方法,那assertEquals就只有一个了:
A expected = new A();expected.setId(xxx);expected.setPath(xxx);...expected.setB(new B());expected.getB().setDesc(xxx);...expected.getB().setC(new C());expected.getB().getC().setV1(xxx);...assertEquals(expected, a);
虽然assertEquals只有一个,但为了建立一个期待的expected作为标尺来比较,需要为提供大量的set方法。这么多set方法带来的问题其实并不比那么多get少。
而且,为A类实现equals方法也是有风险的,因为equals方法本身也需要测试(只要是人写的代码本质上都需要测试!),也需要时间成本。很多类其实没必要去override equals方法。写代码就得维护,没必要写的代码坚决不写,否则维护量更多。同样的,不是所有的代码都能控制的。
1.3 使用Pretty
Pretty类可以很pretty的解决以上调试和单元测试中的问题。在给出Pretty类之前,先从使用者的角度看看她的pretty:
package org.wenzhe.jvlib.debug.test;import static org.junit.Assert.*;import java.io.File;import java.io.IOException;import java.math.BigDecimal;import java.text.DateFormat;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Arrays;import java.util.Calendar;import java.util.Date;import java.util.GregorianCalendar;import java.util.HashMap;import java.util.List;import java.util.Map;import org.junit.Test;import org.wenzhe.jvlib.debug.Pretty;/** * @author liuwenzhe2008@qq.com * */public class PrettyTest { private static class A { private int id = 100; private File path = new File("/home/wenzhe/code/pretty"); private Integer[] array = {86, 755, 1234, 5678}; private List<String> list = Arrays.asList("My", "name", "is", "Wenzhe"); private B b = new B("This is my Pretty Test"); } private static class B { private String desc; private Map<String, Date> map = new HashMap<String, Date>(); private C c = new C(); public B(String desc) { this.desc = desc; SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); try { map.put("Today", dateFormat.parse("2013-06-15")); map.put("Earth Doomsday", dateFormat.parse("2012-12-21")); } catch (ParseException e) { throw new RuntimeException(e); } } } private static class C { private double v1 = 3.14; private BigDecimal v2 = new BigDecimal( "3.141592653589793238462643383279502884197169399"); } @Test public void test1() throws IOException { A a = new A(); assertTrue(Pretty.equalsGolden("test1", a)); } public static void main(String[] args) throws IOException { Pretty.setDebugMode(true); PrettyTest test = new PrettyTest(); test.test1(); }}
1.3.1 Pretty结构
这是一个带有main方法的单元测试类。先撇开单元测试,我们把它当成一个普通java文件来运行(即从main方法开始运行),在屏幕上会打印出对象 a 的pretty结构:
org.wenzhe.jvlib.debug.test.PrettyTest$A { array : [86, 755, 1234, 5678] b : org.wenzhe.jvlib.debug.test.PrettyTest$B { c : org.wenzhe.jvlib.debug.test.PrettyTest$C { v1 : 3.14 v2 : 3.141592653589793238462643383279502884197169399 } desc : This is my Pretty Test map : {Earth Doomsday=Fri Dec 21 00:00:00 PST 2012, Today=Sat Jun 15 00:00:00 PDT 2013} } id : 100 list : [My, name, is, Wenzhe] path : pretty}
根据pretty结构的缩进,可以很容易看出,对象a的类是: org.wenzhe.jvlib.debug.test.PrettyTest类的内部类A,其成员array是一个数组,值为[86, 755, 1234, 5678]。另一个成员 b 是类 (PrettyTest的内部类B)的对象,b 里面的成员变量c 是类(PrettyTest内部类C)的对象……根据缩进,各种成员变量及其嵌套聚合类的对象也都轻易可见。这在开发调试过程中非常好用!
如果以单元测试的方式运行,屏幕上没有任何输出(No news is Good news),JUnit View中出现大家喜爱的绿色条,祝贺你表测试通过了。(一般对于unittest来说,正确的时候是没输出信息的。)
那么程序怎么知道对象a是期望的呢?注意到第61行,Pretty.equalsGolden("test1", a); 对象a实际上是跟一个名字为test1的golden文件做了比较。这个golden文件的所在的目录为: ${project_root}/src/test/resources/golden/pretty/,这是pretty工具的一个convention,当然也可以改成别的目录,但我不推荐改,很多时候遵从“约定优于配置”的原则总是更好的。打开test1文件,你会发现这也是一个pretty结构,跟之前屏幕上输出的完全一样。
你可能奇怪为什么作为普通java类运行屏幕上会打印,而作为unit test却不会打印呢?其实区别并不在于用哪种运行方式,唯一的区别在于是否选择了Pretty类的debug模式。(一般来说,unit test下不启动debug模式,而在开发调试过程中启动)。注意到main函数刚开始的时候(第65行),debug mode设置为true,当Pretty工具要得到对象a的pretty结构时,会将它打印出来,方便调试,省得在代码里面加入print函数的麻烦。debug mode缺省是关的,所以unit test就没有打印出来了。(有兴趣可以阅读后面的源代码)。
1.3.2 Pretty Diff
如果unit test测到对象a与golden文件不同,那会怎样?假如有个大老粗不小心把C类中的成员变量v1删除了,又不小心增加了成员变量v3(取值为true),更是不小心把A类的成员变量list里面insert了一个“NOT”,不管是不是在Pretty的debug模式,屏幕上都会输出:
Diff from Expected to Actual: -: v1 : 3.14+: v3 : true<: list : [My, name, is, Wenzhe]>: list : [My, name, is, NOT, Wenzhe]
Pretty工具的错误输出,够pretty吧,大老粗干了哪些坏事这里一目了然。
1.3.3 Pretty Golden
如果大老粗是故意这么修改的(背后有大老板支持,用软件行业的语言讲就是“需求变了”),那么golden文件也就过时了,需要更新才能让unit test通过。
需要手动更改golden文件吗?我可不干!因为Pretty让我越来越懒了。
懒人都喜欢Pretty,因为Pretty提供了自动更新golden文件的功能。这时候你开启Pretty的debug模式,运行,屏幕上除了输出对象a的pretty结构和Diff信息之外,Pretty还会问你“Overwrite (Y/N)? ”,回答Y即可自动更新golden文件test1。
有了Pretty,你永远不需要手动写golden文件:当golden不存在时,Pretty会帮你创建;当golden存在但有Diff时会提醒你是否需要更新。
1.4 Pretty原理及源码
也许你已经迫不及待地想知道Pretty类是怎么实现的,原理其实也简单,就是通过Java的“反射”机制,把类的成员变量拿出来,放进一个Map里,key为成员变量名,value为成员变量的值,然后递归地输出到一个具有缩进层次的代表pretty结构的字符串里。这是一个既美丽又好用的字符串,在debug模式下打印到标准输出,在unit test下就是与golden文件进行字符串比较,从而避免了做对象比较的麻烦,同时golden文件的pretty结构记录了期待对象完整的层层信息,有助于理解代码,^_^。
package org.wenzhe.jvlib.debug;import java.io.File;import java.io.IOException;import java.lang.reflect.Field;import java.math.BigDecimal;import java.math.BigInteger;import java.util.ArrayList;import java.util.Date;import java.util.List;import java.util.Map;import java.util.TreeMap;import org.wenzhe.jvlib.file.FileUtil;/** * @author liuwenzhe2008@qq.com * */public class Pretty { private static final String TAB = " "; private static boolean debugMode = false; private static boolean showFileAbsPath = false; public static void setDebugMode(boolean toDebug) { debugMode = toDebug; Golden.setDebugMode(debugMode); } public static void setShowFileAbsPath(boolean toShowFileAbsPath) { showFileAbsPath = toShowFileAbsPath; } private static Map<String, Object> obj2map(Object o) { Map<String, Object> props = new TreeMap<String, Object>(); Class<?> c = o.getClass(); for (Field field : c.getDeclaredFields()) { String name = field.getName(); Object value = null; boolean originalAccessible = field.isAccessible(); if (!originalAccessible) { field.setAccessible(true); } try { value = field.get(o); } catch (IllegalArgumentException e) { throw new RuntimeException("Should not reach!", e); } catch (IllegalAccessException e) { throw new RuntimeException("Should not reach!", e); } finally { if (!originalAccessible) { field.setAccessible(false); } } props.put(name, value); } return props; } public static void println(Object obj, int level) { System.out.println(str(obj, level)); } public static String str(Object obj, int level) { return str(obj, level, debugMode); } public static String str(Object obj, int level, boolean debugMode) { String result = str(obj, 0, level); if (debugMode) { System.out.println(result); } return result; } @SuppressWarnings("unchecked") private static String str(Object obj, int tabCnt, int level) { if (obj == null) { return ""; } else if (tabCnt > level || obj instanceof String || obj instanceof BigDecimal || obj instanceof BigInteger || obj instanceof Integer || obj instanceof Short || obj instanceof Long || obj instanceof Float || obj instanceof Double || obj instanceof Boolean || obj instanceof Class || obj instanceof Date ) { return obj.toString(); } else if (obj instanceof File) { File file = (File)obj; if (showFileAbsPath) { return FileUtil.unixPath(file.getAbsoluteFile()); } else { return file.getName(); } } else if (obj instanceof Iterable) { List<String> results = new ArrayList<String>(); for (Object o : (Iterable<?>)obj) { results.add(str(o, tabCnt + 1, level)); } return results.toString(); } else if (obj instanceof Object[]) { List<String> results = new ArrayList<String>(); for (Object o : (Object[])obj) { results.add(str(o, tabCnt + 1, level)); } return results.toString(); } else if (obj instanceof Map) { Map<String, String> results = new TreeMap<String, String>(); for (Map.Entry<Object, Object> entry : ((Map<Object, Object>)obj).entrySet()) { String key = str(entry.getKey(), tabCnt + 1, level); String value = str(entry.getValue(), tabCnt + 1, level); results.put(key, value); } return results.toString(); } else { Map<String, Object> m = obj2map(obj); StringBuilder sb = new StringBuilder(); sb.append(obj.getClass().getName() + " {\n"); String nTabs = tabs(tabCnt + 1); for (Map.Entry<String, Object> entry : m.entrySet()) { sb.append(nTabs); sb.append(entry.getKey()); sb.append(" : "); sb.append(str(entry.getValue(), tabCnt + 1, level)); sb.append("\n"); } sb.append(tabs(tabCnt)); sb.append("}"); return sb.toString(); } } private static String tabs(int count) { StringBuilder sb = new StringBuilder(); while (count-- > 0) { sb.append(TAB); } return sb.toString(); } public static boolean equalsGolden(String goldenFileName, Object obj) throws IOException { return equalsGolden(goldenFileName, obj, 5); } public static boolean equalsGolden(String goldenFileName, Object obj, int level) throws IOException { File goldenFile = new File("src/test/resources/golden/pretty", goldenFileName); return equalsGolden(goldenFile, obj, level); } public static boolean equalsGolden(File goldenFile, Object obj, int level) throws IOException { String actual = str(obj, level, false).trim(); return Golden.equals(goldenFile, actual); }}
在调试过程中,Pretty的str方法和println方法是很常用的;而在unit test中,equalsGolden方法更加方便。
1.5 Pretty姐妹篇:Golden原理及源码
Pretty类用到了另一个相当实用的工具类:Golden,是Pretty的好姐妹,如果golden文件不存在则帮你创建,如果存在了则帮你把字符串跟golden文件做比较,一旦发现差异,则将差异部分打印出来。在Golden类的调试模式下(debugMode=true)还会提示你是否需要overwirte你的golden文件。这是很实用的功能,试想一下如果有上千个golden文件,维护的工作量是很大的。需求变了,代码结构也变了,原先的golden不再正确时就需要更新。要是每次都得手动去文件里查找哪些不同,手动去修改golden文件,那也是相当麻烦的事。Golden类可以给你“一键搞定”的成就感!
package org.wenzhe.jvlib.debug;import java.io.File;import java.io.IOException;import org.wenzhe.jvlib.diff.DiffUtil;import com.google.common.base.Charsets;import com.google.common.io.Files;/** * @author liuwenzhe2008@qq.com * */public class Golden { private static boolean debugMode = false; public static void setDebugMode(boolean toDebug) { debugMode = toDebug; } public static boolean equals(String goldenFileName, String actual) throws IOException { File goldenFile = new File("src/test/resources/golden", goldenFileName); return equals(goldenFile, actual); } public static boolean equals(File goldenFile, String actual) throws IOException { if (debugMode) { System.out.println(actual); } goldenFile = goldenFile.getAbsoluteFile(); if (!goldenFile.isFile()) { System.out.println("Generate golden file: " + goldenFile); Files.createParentDirs(goldenFile); Files.write(actual, goldenFile, Charsets.UTF_8); return true; } String expected = Files.toString(goldenFile, Charsets.UTF_8); if (actual.equals(expected)) { return true; } else { // need 3'rd party: diffutils { System.out.println("Diff from Expected to Actual: "); System.out.println(DiffUtil.diff(expected, actual)); if (debugMode) { System.out.print("Overwrite (Y/N)? "); char in = (char)System.in.read(); if (in == 'Y' || in == 'y') { Files.write(actual, goldenFile, Charsets.UTF_8); return true; } } // } return false; } }}
2. Pretty之Python版
Python的实现方法非常简单,自带的pprint方法就可以实现pretty print,因此要做到主要是将object转换成dict(即Java里的Map),而Python自带的vars函数返回的就是成员变量的dict。源码如下:
# author: liuwenzhe2008@qq.comimport pprintfrom StringIO import StringIOdef obj2map(o): """ if o doesn't have __dict__, not return map """ if hasattr(o, "__dict__"): m = vars(o) return obj2map(m) elif type(o) == dict: m = {} for k,v in o.items(): key = obj2map(k) if not key.__hash__: key = str(key) m[key] = obj2map(v) return m elif type(o) == list: arr = [] for item in o: arr.append(obj2map(item)) return arr elif type(o) == tuple: return tuple(obj2map(list(o))) else: return o def printObj(o, stream=None): """Pretty-print a mapped Python object to a stream [default is sys.stdout].""" pprint.pprint(obj2map(o), stream) def obj2str(o): s = StringIO() printObj(o, s) return s.getvalue()
由于Python的动态脚本语言特性,我们可以在运行时导入Pretty,然后打印感兴趣的对象。下面是Pretty在pdb调试中的例子。
pdb> import Prettypdb> Pretty.printObj(xxx)
Python版的Pretty,输出结果也是同样pretty,请看下面的unit test文件,特别是复杂类A的对象a所对应的pretty结构,即字符串expectedStrA
# author: liuwenzhe2008@qq.comimport unittestimport Prettyclass A: def __init__(self): self.a1 = "a" self.a2 = 2 self.b = B() self.c = [C(1), C(2)] class B: def __init__(self): self.bm = {3: C(3), "4": C(4), C(7):C(8)} self.bt = (C(5), C(6))class C: def __init__(self, c): self.c = c self.cs = str(c) expectedMapA = \{'a1': 'a', 'a2': 2, 'b': {'bm': {3: {'c': 3, 'cs': '3'}, '4': {'c': 4, 'cs': '4'}, "{'cs': '7', 'c': 7}": {'c': 8, 'cs': '8'}}, 'bt': ({'c': 5, 'cs': '5'}, {'c': 6, 'cs': '6'})}, 'c': [{'c': 1, 'cs': '1'}, {'c': 2, 'cs': '2'}]}expectedStrA = """{'a1': 'a', 'a2': 2, 'b': {'bm': {3: {'c': 3, 'cs': '3'}, '4': {'c': 4, 'cs': '4'}, "{'cs': '7', 'c': 7}": {'c': 8, 'cs': '8'}}, 'bt': ({'c': 5, 'cs': '5'}, {'c': 6, 'cs': '6'})}, 'c': [{'c': 1, 'cs': '1'}, {'c': 2, 'cs': '2'}]}""" class Test(unittest.TestCase): def setUp(self): self.a = A() def testObj2map(self): m = Pretty.obj2map(self.a) self.assertEqual(expectedMapA, m) self.assertEqual(type(B()), type(self.a.b)) def testObj2Str(self): s = Pretty.obj2str(self.a) self.assertEqual(expectedStrA.strip(), s.strip()) Pretty.printObj(self.a)if __name__ == "__main__": unittest.main()
3. Pretty之Groovy版
Groovy是语法简化、但却功能扩展的Java,思路是一样的,只是代码写起来简单一些(比如反射、格式化等)。源码如下:
package org.wenzhe.gvlib/** * Pretty print an object detailed to string, console * * @author liuwenzhe2008@qq.com * */class Pretty { private static final String TAB = " " * 2 static String str(obj) { return str(obj, false) } static String str(obj, boolean recursive) { return strLevel(obj, (recursive ? 1 : 0)) } private static String strLevel(obj, int tabLevel) { if (obj == null || obj instanceof String || obj instanceof BigDecimal || obj instanceof BigInteger || obj instanceof Integer || obj instanceof Short || obj instanceof Long || obj instanceof Float || obj instanceof Double || obj instanceof Boolean || obj instanceof Class ) { return obj.toString() } if ( obj instanceof List || obj instanceof Object[] || obj instanceof Set ) { List<String> prettyList = obj.collect { if (tabLevel <= 0) { return str(it) } else { return strLevel(it, tabLevel + 1) } } return prettyList.toString() } if (obj instanceof Map) { if (!obj) { return obj.toString() } List<String> list = obj.collect() { key, val -> if (tabLevel <= 0) { return str(key) + ' : ' + str(val) } else { return str(key) + ' : ' + strLevel(val, tabLevel + 1) } } return str(list) } Map<String, Object> props = obj.getProperties() if (tabLevel <= 0) { return props.inject(obj.class.name + ' {\n') { buf, entry -> if (entry.key == "class") { return buf; } else { return buf + TAB + "$entry.key : $entry.value\n" } } + "}" } else { return props.inject(obj.class.name + ' {\n') { buf, entry -> if (entry.key == "class") { return buf; } else { return buf + strFormat(entry.key, entry.value, tabLevel) } } + TAB * (tabLevel - 1) + "}" } } private static String strFormat(String key, Object value, int tabLevel) { String s = strLevel(value, tabLevel + 1) return TAB * tabLevel + "$key : $s\n"; } static String listMethodObjs(obj) { return Pretty.str(obj.metaClass.methods) } static String listMethodDescs(obj) { List<String> methods = obj.metaClass.methods.cachedMethod*.toString() return formatMethodList(obj, methods) } static String listMethodNames(obj) { List<String> methods = obj.metaClass.methods.cachedMethod*.name.sort().unique() return formatMethodList(obj, methods) } static private String formatMethodList(obj, List<String> methods) { return """${obj.class.name} { ${methods.join("\n ")}}""" }}
4. Pretty之C++设计思路
由于C++没有“反射”机制,要想获取类的所有私有(或公有)成员变量的名字与类型并不容易。但思路还是有的,比如可以通过分析C++类的源代码来获得,可以借助第三方库,如Clang来实现。Clang由Apple开发,BSD开源授权,支持C,C++,Object C,Object C++等编程语言,能够对源代码进行词法和语意分析,结果为抽象语法树。通过抽象语法树,我们可以模仿类似与Java中“反射”机制,来得到类的成员信息(名字,类型,取值等)。只是一个思路,有兴趣的朋友不妨一试。
---------------------- 本博客所有内容均为原创,转载请注明作者和出处 -----------------------
作者:刘文哲
联系方式:liuwenzhe2008@qq.com
博客:http://blog.csdn.net/liuwenzhe2008
- Pretty工具类:让软件开发调试与单元测试更happy!
- 安卓开发工具优化APK,让软件更流畅
- 安卓开发工具优化APK,让软件更流畅
- 实用互联网软件开发与调试工具
- 让iOS开发变得更有效率-分类、工具类
- 让iOS开发变得更有效率-分类、工具类
- 让iOS开发变得更有效率-分类、工具类
- 让iOS开发变得更有效率-分类、工具类
- 文件缓存的工具类,让你开发更轻松
- 开发工具与调试》
- 软件调试及单元测试
- 软件单元测试工具比较
- 软件单元测试工具比较
- 软件单元测试工具比较
- 软件单元测试工具比较
- 软件单元测试工具比较
- 软件测试--单元测试工具
- 常见软件单元测试工具
- eclipse里设置运行参数
- JS--命名空间的理解(namespace)
- 山寨Junit
- java.io.InvalidClassException:CartonPrintVO; local class incompatible: stream classdesc serialVersio
- Mac OS X 安装mysql过程
- Pretty工具类:让软件开发调试与单元测试更happy!
- q
- ubuntu 13.04 web开发从零配置到全装备手记(环境搭建全攻略)
- 想讨论一个指定必经节点求最短路径思路,大伙有什么思路没有?
- QT串口通信
- Factory Method模式
- linux mysql运维一些常用记录
- 熬之滴水穿石:JSP--HTML中的JAVA代码(1)
- MySQL存储过程中的3种循环