单元测试
测试是非常重要的。但是,如果你不是负责测试的人员,一般你做好单元测试就已经足够了
什么是单元测试
单元测试,通俗易懂地讲,就是编写测试来验证某一个模块的功能正确性,一般会指定输入,验证输出是否符合预期。
单元测试一定需要用到 unittest 模块,下面是一个使用示例:
import unittest
# 将要被测试的排序函数
def sort(arr):
l = len(arr)
for i in range(0, l):
for j in range(i + 1, l):
if arr[i] >= arr[j]:
tmp = arr[i]
arr[i] = arr[j]
arr[j] = tmp
# 编写子类继承unittest.TestCase
class TestSort(unittest.TestCase):
# 以test开头的函数将会被测试
def test_sort(self):
arr = [3, 4, 1, 5, 6]
sort(arr)
# assert 结果跟我们期待的一样
self.assertEqual(arr, [1, 3, 4, 5, 6])
if __name__ == '__main__':
## 如果在Jupyter下,请用如下方式运行单元测试
unittest.main(argv=['first-arg-is-ignored'], exit=False)
## 如果是命令行下运行,则:
## unittest.main()
## 输出
..
----------------------------------------------------------------------
Ran 2 tests in 0.002s
OK
创建一个用于测试的类,这个类要继承 unittest.TestCase
所有以 “test” 开头的函数都会被测试
在测试的内部,通常使用 “assert” 开头的属性进行结果验证。如代码中是判定:arr 是否和 [1,3,4,5,6] 相等
使用 unittest.main( ) 启动测试
运行输出 OK,代表测试通过
单元测试的技巧
我们仔细想一下单元测试的核心:测试某一小块代码的功能。但是在实际生产中,该模块会有非常多的依赖项,搞定这些依赖项是非常麻烦的。所以单元测试的技巧就在这里:用虚假的实现,替换被测试函数的一些依赖项,让我们可以更加专注于需要被测试的功能上。
实现这种虚假实现有三种方法:mock、side_effect、patch
1.mock
import unittest
from unittest.mock import MagicMock
class A(unittest.TestCase):
def m1(self):
val = self.m2()
self.m3(val)
def m2(self):
pass
def m3(self, val):
pass
def test_m1(self):
a = A()
a.m2 = MagicMock(return_value="custom_val")
a.m3 = MagicMock()
a.m1()
self.assertTrue(a.m2.called) # 验证m2被call过
a.m3.assert_called_with("custom_val") # 验证m3被指定参数call过
if __name__ == '__main__':
unittest.main(argv=['first-arg-is-ignored'], exit=False)
## 输出
# ..
# ----------------------------------------------------------------------
# Ran
# 2
# tests in 0.002
# s
#
# OK
unittest.mock.MagicMock 可以创建一个虚假函数,虚假函数的功能一般来说比较单一
这个虚假函数可以替代其它函数
你可以验证某个 mock 实例的调用信息
2.Mock Side Effect
from unittest.mock import MagicMock
def side_effect(arg):
if arg < 0:
return 1
else:
return 2
mock = MagicMock()
mock.side_effect = side_effect
print(mock(-1))
# 1
print(mock(1))
# 2
print(mock.called)
# True
mock.side_effect 可以设置函数的行为,这让虚假函数有了更多功能
3.patch
patch 给开发者提供了遍历的函数 mock 方法。可以利用 装饰器 或 上下文管理环境 实现mock。
# patc.py
def func_1():
return 1
def func_call():
x = func_1()
return x
if __name__ == '__main__':
f = func_call()
print(f)
# 测试.py
from unittest.mock import patch
import unittest
import patc
res = 100
class GetPatchTest(unittest.TestCase):
@patch('patc.func_1') # 我要使用 patch功能 替换掉 patc.func_1 函数
def test_patc(self, mock_1): # 使用 mock_1 替换 func_1,mock_1 已经是一个虚假函数
mock_1.return_value = res # 为虚假函数设置返回值
self.assertEqual(patc.func_call(), 100) # 判断是否符合预期结果
if __import__ == '__main__':
unittest.main()
在这个测试中,我们使用 mock_1替代了 patc.func_1 函数
提高质量的关键
测试用例对测试代码的覆盖率,使用 coverage 模块
将代码进行模块化分解,这样既有利于阅读,又利于测试
debug
通常我们可以使用 ide 中自带的断点进行 debug ,但是有时候我们的工作环境可能没有现成的工具。在这种情况下我们可以使用 pdb模块 进行 debug。
a = 1
b = 2
import pdb
pdb.set_trace()
c = 3
print(a + b + c)
在程序运行到 pdb.set_trace( ) 之后,会停下:
> /Users/jingxiao/test.py(5)()
-> c = 3
这时你可以使用命令进行一些操作:
p x:打印变量 x
n :执行下一行代码
l :查看上下 11 行代码,让程序员了解代状态
s :进入相应代码块(比如一个函数或一个模块),进入会显示 --call-- ,退出代码块会出现 --return-- 字样。
def func():
print('enter func()')
a = 1
b = 2
import pdb
pdb.set_trace()
func()
c = 3
print(a + b + c)
# pdb
> /Users/jingxiao/test.py(9)()
-> func()
(pdb) s
--Call--
> /Users/jingxiao/test.py(1)func()
-> def func():
(Pdb) l
1 -> def func():
2 print('enter func()')
3
4
5 a = 1
6 b = 2
7 import pdb
8 pdb.set_trace()
9 func()
10 c = 3
11 print(a + b + c)
(Pdb) n
> /Users/jingxiao/test.py(2)func()
-> print('enter func()')
(Pdb) n
enter func()
--Return--
> /Users/jingxiao/test.py(2)func()->None
-> print('enter func()')
(Pdb) n
> /Users/jingxiao/test.py(10)()
-> c = 3
r :跳出代码块,当前代码块的代码会继续执行
b :设置断点,例如,b 11 可以在代码中的第 10 行添加一个断点
c :继续执行程序,直到遇到下一个断点
其它命令可以查看官方文档
性能
程序运行时,性能的瓶颈可能只受限于某一个模块,如果知道了哪个模块拉低了程序性能,我们就可以对症下药了。
在 python 中,使用 cProfile 可以实现对每个模块的性能分析,例如,下面编写一个计算斐波那契数列的程序:
def fib(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n-1) + fib(n-2)
def fib_seq(n):
res = []
if n > 0:
res.extend(fib_seq(n-1))
res.append(fib(n))
return res
fib_seq(30)
我们发现这个程序有点慢,我们对代码做如下改动:
import cProfile
# def fib(n)
# def fib_seq(n):
cProfile.run('fib_seq(30)')
或者,你可以在命令行运行时使用特定的参数:
python3 -m cProfile xxx.py
你会获得一个性能分析表:
性能分析
各项参数含义如下:
ncalls :相应代码、函数被调用的次数
tottime :对应代码、函数执行的时间(不包含调用其它代码的时间)
tottime percall :每次代码执行的时间,即 tottime / ncalls
cumtime : 对应代码执行的时间(包含调用)
cumtime percall :每次代码执行时间,即 cumtime / ncalls
你会发现,性能瓶颈出现在第二行的 fib( ) 函数,它被调用了 700w+ 次。
我们使用一个 装饰器 + 字典 设置一个调用备忘录,这样可以减小调用次数:
def memoize(f):
memo = {}
def helper(x):
if x not in memo:
memo[x] = f(x)
return memo[x]
return helper
@memoize
def fib(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n-1) + fib(n-2)
def fib_seq(n):
res = []
if n > 0:
res.extend(fib_seq(n-1))
res.append(fib(n))
return res
fib_seq(30)
再分析其性能,结果如下:
优化后性能