作者 | 豌豆花下猫
责编 | 郭芮
在 Python 中,实现参数化测试的几个库,是如何做到把一个方法变成多个方法,并且将每个方法与相应的参数绑定起来的呢? 我们再提炼一下:在一个类中,如何使用装饰器把一个类方法变成多个类方法(或者产生类似的效果)?# 带有一个方法的测试类
class TestClass:
def test_func(self):
pass
# 使用装饰器,生成多个类方法
class TestClass:
def test_func1(self):
pass
def test_func2(self):
pass
def test_func3(self):
pass
Python 中装饰器的本质就是移花接木,用一个新的方法来替代被装饰的方法。在实现参数化的过程中,我们介绍过的几个库到底用了什么手段/秘密武器呢?
![5e24abe8fc5fa4e412ed47ec09105674.png](https://i-blog.csdnimg.cn/blog_migrate/ac808b0b94e8d6cce1abaa34b813f5eb.png)
import unittest
from ddt import ddt,data,unpack
@ddt
class MyTest(unittest.TestCase):
@data((3, 1), (-1, 0), (1.2, 1.0))
@unpack
def test(self, first, second):
pass
ddt 可提供 4 个装饰器:1 个加在类上的 @ddt,还有 3 个加在类方法上的 @data、@unpack 和 @file_data(前文未提及)。
先看看加在类方法上的三个装饰器的作用:
# ddt 版本(win):1.2.1
def data(*values):
global index_len
index_len = len(str(len(values)))
return idata(values)
def idata(iterable):
def wrapper(func):
setattr(func, DATA_ATTR, iterable)
return func
return wrapper
def unpack(func):
setattr(func, UNPACK_ATTR, True)
return func
def file_data(value):
def wrapper(func):
setattr(func, FILE_ATTR, value)
return func
return wrapper
它们的共同作用是在类方法上 setattr() 添加属性。至于这些属性在什么时候使用?下面看看加在类上的 @ddt 装饰器源码:
![73cf5896575bc2d91dac7f8696b1d21b.png](https://i-blog.csdnimg.cn/blog_migrate/07da69f4ecaf4be12738ee218157fe21.jpeg)
![2faf9ed8c4df4a08cae9c5ea45134bfa.png](https://i-blog.csdnimg.cn/blog_migrate/edbf924bd0e6a6a1faa260d4736e3162.jpeg)
遍历类方法的参数键值对
根据原方法及参数对,创建新的方法名
获取原方法的文档字符串
对元组和列表类型的参数作解包
在测试类上添加新的测试方法,并绑定参数与文档字符串
![187501d3bd091b038944f2b7915d2766.png](https://i-blog.csdnimg.cn/blog_migrate/1c6ff2e94d3416f39139bd4c77d3d6c5.png)
import unittest
from parameterized import parameterized
class MyTest(unittest.TestCase):
@parameterized.expand([(3,1), (-1,0), (1.5,1.0)])
def test_values(self, first, second):
self.assertTrue(first > second)
它提供了一个装饰器类 @parameterized,源码如下(版本 0.7.1),主要做了一些初始的校验和参数解析,并非我们关注的重点,略过。
![7a30915bef0817dcaa2c24ea4e50fefd.png](https://i-blog.csdnimg.cn/blog_migrate/c96036d0808a3027f7e48a5ca8b2e431.jpeg)
A "brute force" method of parameterizing test cases. Creates new test cases and injects them into the namespace that the wrapped function is being defined in. Useful for parameterizing tests in subclasses of 'UnitTest', where Nose test generators don't work.关键的两个动作是:“creates new test cases(创建新的测试单元)”和“inject them into the namespace…(注入到原方法的命名空间)”。 关于第一点,它跟 ddt 是相似的,只是一些命名风格上的差异,以及参数的解析及绑定不同,不值得太关注。
![2f965fac332c11701090ba6196191af3.png](https://i-blog.csdnimg.cn/blog_migrate/e2762f5689eaa0d9d8bed434eb05811a.jpeg)
![cfcea8c97bc46b780d71e16c1c2f5145.png](https://i-blog.csdnimg.cn/blog_migrate/8cc97dcc7fcb8681663cd891f6916e19.jpeg)
![ce4c539f99aeb7cde6d23f64a8517ddd.png](https://i-blog.csdnimg.cn/blog_migrate/fb8e50d3f79152f8c3affdf128b975b1.png)
import pytest
@pytest.mark.parametrize("first,second", [(3,1), (-1,0), (1.5,1.0)])
def test_values(first, second):
assert(first > second)
首先看到“mark”,pytest 里内置了一些标签,例如 parametrize、timeout、skipif、xfail、tryfirst、trylast 等,还支持用户自定义的标签,可以设置执行条件、分组筛选执行,以及修改原测试行为等等。
用法也是非常简单的,然而,其源码可复杂多了。我们这里只关注 parametrize,先看看核心的一段代码:
![2f9b208b705db63fc28f624ddc4c408a.png](https://i-blog.csdnimg.cn/blog_migrate/005a909eca9f96adb0e1497c4786eea3.jpeg)
![283cd7ee5a8244a9d3fa4859e465269f.png](https://i-blog.csdnimg.cn/blog_migrate/d2a0773c9c278ac89cd4641b254824af.jpeg)
![2621b095b57f9a2107ecc773ab751a90.png](https://i-blog.csdnimg.cn/blog_migrate/7f8d338583f562bce60e7c0ad4351e7b.jpeg)
![d88edc610cfe63e7c70c2c90900f7e87.png](https://i-blog.csdnimg.cn/blog_migrate/bc3dd7583ac1f6583618d94253865a20.jpeg)
![0e61fa3c7edaa998da04b189db1037b5.png](https://i-blog.csdnimg.cn/blog_migrate/dbdb9d30c7877bd723fdbd4007ed52d2.png)
![20baadac1436d18407cb145f776113fe.png](https://i-blog.csdnimg.cn/blog_migrate/bd038fdccc58b1da8f98ac354c1ac61f.png)