unit-test

unit-test

自动化测试

不同于C和Java等语言编写的程序,Python只有在运行中才会检查一些错误,因此只有在运行和测试程序时,才会知道它是否能够正常的运行。
为了解决这个问题,就有了用于测试,调试和探查Python代码的技术和库模块。

文档字符串和doctest模块

如果函数,类,模块的第一行是一个字符串,那么这个字符串就是文档字符串,当调用help()时,这些文档将会展示出来。
由于程序员倾向于在交互式shell中进行试验时查看文档字符串,所以这些字符串中通常会包含简短的交互式例子

splitter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def split(line, types=None, delimiter=None):
"""Splits a line of text and optionally performs type conversion.
For example:
>>> split('GOOG 100 490.50')
['GOOG', '100', '490.50']
>>> split('GOOG 100 490.50', [str, int, float])
['GOOG', 100, 490.5]

By default, splitting is performed on whitespace, but a different
delimiter can be selected with the delimiter keyword argument:

>>>split('GOOG,100,490.50', delimiter=',')
['GOOG', '100', '490.50']
>>>
"""
fields = line.split(delimiter)
if types:
fields = [ty(val) for ty, val in zip(types, fields)]
return fields

上面这个demo中,封装了一下Python的split函数,使其可以直接在切完后进行类型转换,就写了一个文档来告诉看这个代码或者使用help的程序员

但这样存在一个问题,如果忘记更改这个文档字符串了怎么办?可以使用doctest模块来解决这个问题。我们可以新建一个.py文件来测试它,或者直接测试本身

1
2
3
4
5
6
7
8
9
# 新开文件测试
import splitter
import doctest
nfail, ntests = doctest.testmod(splitter) # 指定模块上运行测试

# 直接测试本身
if __name__ == '__main__':
import doctest
doctest.testmod()

doctest要求函数输出与从交互解释器得到的输出完全一致,所以需要特别重视精度问题

单元测试和unittest模块

对于更全面的程序测试,可以使用unittest模块来进行测试。如果进行单元测试,开发人员会为程序每个组成元素(函数,类,方法,模块)编写独立的测试案例。然后运行这些测试来组成更大的测试框架和工具

我们试着编写单元测试来测试我们封装的split()函数

testsplitter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import unittest
import splitter

class TestSplitterFunction(unittest.TestCase):
def setUp(self) -> None:
# 执行设置操作
pass

def tearDown(self) -> None:
# 执行清除操作
pass

def test_simple_string(self):
r = splitter.split('GOOG 100 490.50')
self.assertEqual(r, ['GOOG', '100', '490.50'])

def test_type_convert(self):
r = splitter.split('GOOG 100 490.5', [str, int, float])
self.assertEqual(r, ['GOOG', 100, 490.50])

def test_delimiter(self):
r = splitter.split('GOOG,100,490.50', delimiter=',')
self.assertEqual(r, ['GOOG', '100', '490.50'])

if __name__ == '__main__':
unittest.main()

运行时完毕得到了这样的结果,说明样例的测试成功

Ran 3 tests in 0.003s
OK

unittest的基本使用包括定义一个继承自unittest.TestCase的类,这个类中,各种测试由以名称test开头的方法定义,如上面的函数(可以随意命名,但必须是以test来开头),在各个测试中,使用断言来检查不同的条件

经常使用的是unittest.TestCase的实例t的以下方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 在运行任何测试方法前, 调用它来执行设置步骤
t.setUp()

# 在运行测试之后,调用它来执行清除操作
t.tearDown()

# 比较是否相等
t.assertEqual(expr [, msg])


# 如果expr的计算结果为false,发出提示msg
t.failUnless(expr [, msg])

# x,y不相等就测试失效
t.faillIfEqual(x, y [, msg])
t.assertNotEqual(x, y [, msg])

# 如果数字x和y未包含在对方的places小数位中, 则表明测试失败
t.faillIfAlmostEqual(x, y [,places [, msg]])
t.assertNotAlmostEqual(x, y [,places [, msg]])

# 如果可调用对象callable未引发异常exc,表示测试失效。可以使用元组传递多个异常类型
t.assertRaises(exc, callable, ...)
t.failUnlessRaises(exc, callable, ...)

# 当expr的布尔值为true时表示测试失效
t.failIf(expr [, msg])

# 表明测试失败
t.fail([msg])

调优与优化

进行计时测量

如果只是想要对长时间运行的Python程序进行计时,最简单的方法通常是在UNIX time等命令的控制下运行它。如果需要对一组长时间运行的语句进行计时,可以插入time.clock()的调用来获取CPU时间的最新读数,或者插入time.time()的调用来读取最新的时钟时间

当然如果仅仅想对一个特定的语句进行测试,可以使用timeit模块的timeit(code [, setup])函数
如:

1
2
3
4
5
>>> from timeit import timeit 
>>> timeit('math.sqrt(2.0)', 'import math')
>>> 0.1086882
>>> timeit('sqrt(2.0)', 'from math import sqrt')
>>> 0.09106120000000217

setup语句用于设定环境,该函数会报告执行时间(使用默认执行次数),当然使用number=count参数也可以自己指定测试的重复次数

进行内存测量

sys模块有getsizeof()函数,用于分析各个Python对象内存的占用(以字节为单位)

1
2
3
4
5
6
7
8
9
>>> import sys
>>> sys.getsizeof(1)
28
>>> sys.getsizeof("Hello World")
60
>>> sys.getsizeof([1,2,3,4])
96
>>> sum(sys.getsizeof(x) for x in [1,2,3,4])
112
  • 对于列表和元组,字典等容器,报告的大小只是容器对象本身的大小,而不是容器中包含的所有对象的累计大小。

  • getsizeof()只是粗略的计算内存使用量,而在内部,解释器会通过引用计数来频繁的共享对象,所以消耗的实际内存会比想象中的小。

ps:为什么int型有28个字节
参考
int 类型在python中是动态长度的。因为python3中int类型是长整型,理论支持无限大的数字,但它的结构其实也很简单, 在 longintepr.h 中定义:

1
2
3
4
struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
};

这结构是什么意思呢,重点在于 ob_digit 它是一个数组指针。digit 可认为是 int 的别名。python的整型存储机制是这样的。比方要表示一个很大的数: 123456789 。而每个元素最大只能表示3位十进制数(为理解打的比方)。那么python就会这样存储:

1
2
3
ob_digit[0] = 789
ob_digit[1] = 456
ob_digit[2] = 123

低位存于低索引下。python中整型结构中的数组,每个元素最大存储 15 位的二进制数(不同位数操作系统有差异32位系统存15位,64位系统是30位)。

因此,sys.getsizeof(0) 数组元素为0。此时占用24字节(PyObject_VAR_HEAD 的大小)。 sys.getsizeof(456) 需使用一个元素,因此多了4个字节。

反汇编

dis模块可以将Python函数,方法,类反汇编为低级的解释器指令。使用dis模块的dis()函数

调优策略

  • 使用slot来限制实例内存

  • 尽量避免使用(.)进行属性查找, 比较常用的是当大量使用某个库的某个函数是,from xxx import xx而非import xxx, 使用xxx.xx来访问

  • 避免对常见情况使用异常,使用if来规避异常,使用异常来处理不常见的情况

  • 鼓励函数式编程和迭代,多使用列表推导,生成器表达式,生成器,协程,闭包。手动迭代数据往往不如使用列表推导和生成器

  • 使用装饰器和元类用于修改函数和类

Author

Ctwo

Posted on

2019-09-17

Updated on

2020-10-25

Licensed under

Comments