前两天在GitHub上浏览Python的三方库时,看到了下面这个,好像是https的绿锁标志。看起来很可信,让人安心。许多开源项目都有这些图标。看到覆盖率是98%,我心生疑惑。这个是人工统计的,还是程序自动测试的?如果是人工统计,肯定写得更高,这样的数据就没有价值了。如果是程序自动测试出来了,一想就复杂了。它是如何实现的?带着这些疑问,我点击了覆盖率98%,跳转到了https://coveralls.io/页面。经过一番摸索,原来这是一个叫coveralls的三方库实现的,用来实时在线展示单元测试的覆盖率,通过覆盖率跑出测试数据。出于好奇,我安装了pipinstall,拿了我之前的程序,写了几个单元测试,用了这两个命令:coveragerun--source=dbinterface-mpytesttests/coveragereport-m发现这个单元测试的覆盖率果然不错,它由程序自动计算。覆盖率真的很棒。有了这个,写单元测试就不能再偷懒了,代码质量也会有量化的标准。从上图中可以看出文件中有哪些代码行没有被测试,然后据此编写单元测试。也可以生成html文件进行查询,更加直观。猜测coverage应该记录pytest调用的代码行数,然后从所有代码行记录中去掉测试过的行记录,即未测试过的代码行,从而计算覆盖率。当时我不由自主地发了一句‘他妈的牛批’,但我还是有疑惑,程序是怎么检测到执行了哪几行代码的?虽然知道调试的时候可以看到,但是还是不知道怎么写程序统计。知道。好奇心驱使我探索。先看看这个coverage来自哪里,里面有什么:(py38env)?dbinterfacegit:(master)?whichcoverage/Users/aaron/py38env/bin/coverage可以看到coverage的内容:(py38env)?dbinterfacegit:(master)?cat/Users/aaron/py38env/bin/coverage#!/Users/aaron/py38env/bin/python3#-*-coding:utf-8-*-importreimportsysfromcoverage.cmdlineimportmainif__name__=='__main__':sys.argv[0]=re.sub(r'(-script\.pyw|\.exe)?$','',sys.argv[0])sys.exit(main())(py38env)?dbinterfacegit:(master)?其实在命令行执行coverage相当于执行:/Users/aaron/py38env/bin/python3coverage将文件保存在一个目录下,命名为main.py,然后使用PyCharmIDE开始调试。调试过程中,发现coveragerun--source=dbinterface-mpytesttests/命令会将测试结果写入.coverage文件,然后执行coveragereport-m从文件中计算覆盖率。也就是说,关键是搞清楚命令coveragerun--source=dbinterface-mpytesttests/的执行过程。继续Debug,这里说一下,由于我们的命令是在路径/Users/aaron/github/somenzz/dbinterface下执行的,所以在Debug之前,先用os.chdir更改程序的工作目录:main.py#!/Users/aaron/py38env/bin/python3#-*-coding:utf-8-*-importreimportsysfromcoverage.cmdlineimportmainimportosif__name__=='__main__':os.chdir('/Users/aaron/github/somenzz/dbinterface')sys.argv[0]=re.sub(r'(-script\.pyw|\.exe)?$','',sys.argv[0])print(sys.argv[0])sys.exit(main())然后添加参数,设置断点,跟踪。终于追到了这里:这里的tracer类是CTracer,从collector.py文件中的这段代码可以看出其来源:可以看出tracer的原型要么是CTracer,要么是PyTracer。从作者的评论可以看出,CTracer非常快,而PyTracer相对较慢。想看CTracer的源码,结果发现这个file.so文件相当于windows的dll文件。它是一个动态链接库。需要反编译成汇编语言,然后分析执行逻辑。这对我来说太难了。对编译不熟悉,就放弃了。那么就只剩下PyTracer了。原理应该类似。PyTracer的源代码是pytracer.py文件,可以直接打开查看。在文件的开头,导入了以下三个库:importatexitimportdisimportsysfromcoverageimportenv其中,前三个是标准库,atexit是一个退出处理器,可以注册一个函数,在解释器终止时执行。dis是一个Python字节码反汇编器。这两个只用了一次,用处不大,可以忽略。重点是第三个sys模块。这个模块和os模块可以说博大精深。许多程序都会用到它。规则也可以从包的名称来概括。名字越短越重要,使用频率也越高。查看PyTracer的源码,sys.settrace起着决定性的作用,能够统计单元测试覆盖率是coverage的关键。下面是Python官方文档中对sys.settrace的介绍:sys.settrace(tracefunc)用于设置系统的trace功能,方便用户在Python中实现一个Python源码调试器。此函数特定于单个线程,因此对于支持多线程的调试器,必须为每个正在调试的线程使用settrace()注册跟踪函数,或使用threading.settrace()。跟踪函数应接收三个参数:frame、event和arg。frame是当前堆栈帧。事件是一个字符串:'call'、'line'、'return'、'exception'或'opcode'。arg取决于事件类型。bb的官方文档那么多,说实话我看不懂,怎么用?我在网上找了一个例子,比如文件trace.py的内容如下:importsysdefstuff():print("callingstuff")defprinter(frame,event,arg):print(frame,event,arg)returnprinter#returnitselftokeeptracingsys.settrace(printer)stuff()也就是说,在执行函数之前,添加sys.settrace。执行这个文件,可以得到如下结果:,file'trace.py',line4,codestuff>returnNone(py38env)?tmp程序执行的行数,执行的操作完整显示。将这些数据保存到一个文件中,然后就可以进行单元测试覆盖率统计了。虽然无法方便的查询到CTracer的源码,但是还是从PyTracer那里学到了coverage统计单元测试覆盖率的统计方法。一次与工作服的偶遇,让我意识到Python也可以统计代码的执行情况,真是太棒了。趁热打铁,我还发布了一个工作服状态图标的工具库:dbinterface,单元测试覆盖率89%:这是一个数据库操作的通用接口,使用起来相当简单,而且是从现在开始读写各种数据库不是问题:fromdbinterface.database_clientimportDataBaseClientFactoryclient1=DataBaseClientFactory.create(dbtype="postgres",host="localhost",port=5432,user="postgres",pwd="121113",database="postgres",)client2=DataBaseClientFactory.create(dbtype="mysql",host="localhost",port=3306,user="aaron",pwd="aaron",database="information_schema",)result1=client1.read("selectcurrent_date")rows_affeted=client1.write("insertintotmp_test_tablevalues(%s,%s)",("1","aaron"))rows_export=client.export("select*frominformation_schema.TABLES",params=(),file_path="/Users/aaron/tmp/mysql_tables.txt",delimeter="0x02",quote="0x03",all_col_as_str=False,)assertrows_export>0项目地址:https://github.com/somenzz/数据库接口ace本文转载自微信公众号“蟒七”,您可以通过以下二维码关注转载文章,请联系蟒七公众号。
