📝 面试求职: 「面试试题小程序」 ,内容涵盖 测试基础、Linux操作系统、MySQL数据库、Web功能测试、接口测试、APPium移动端测试、Python知识、Selenium自动化测试相关、性能测试、性能测试、计算机网络知识、Jmeter、HR面试,命中率杠杠的。(大家刷起来…)
📝 职场经验干货:
ddt 库运行原理猜想
ddt(Data-Driven Tests)库是一个Python模块,它通过装饰器扩展了unittest测试框架,使得可以在单个测试方法上应用多组参数进行测试。ddt提供了@ddt类装饰器和@data、@unpack、@file_data方法装饰器,用于实现数据驱动测试。
下面是一个 ddt 的使用示例,在测试类上添加 @ddt 装饰器,测试用例方法上添加 @data 装饰器并传入需要的数据:
# ddt_demo1.py
import unittest
from ddt import ddt, data
@ddt
class TestDDTDemo(unittest.TestCase):
@data("江雨桐", "叶知秋", "柳如烟", "顾清歌")
def test_ddt_data(self, value):
print(value)
if __name__ == '__main__':
unittest.main(verbosity=2)
我们先来运行一下 ddt_demo1.py 文件下测试用例,查看执行结果:
PS E:\LearnProject> python .\ddt_demo1.py
test_ddt_data_1_江雨桐 (__main__.TestDDTDemo) ... 江雨桐
ok
test_ddt_data_2_叶知秋 (__main__.TestDDTDemo) ... 叶知秋
ok
test_ddt_data_3_柳如烟 (__main__.TestDDTDemo) ... 柳如烟
ok
test_ddt_data_4_顾清歌 (__main__.TestDDTDemo) ... 顾清歌
ok
----------------------------------------------------------------------
Ran 4 tests in 0.001s
OK
从结果中可以看到,一共执行了4条测试用例,依次是:test_ddt_data_1_江雨桐、test_ddt_data_2_叶知秋、test_ddt_data_3_柳如烟、test_ddt_data_4_顾清歌。
刚好对应@data装饰器传入的参数,测试用例的名称为:{测试用例方法名}_{参数序号}_{参数}。
从中不难猜出ddt库的可能实现原理:首先获取传入的数据,并根据一定的规则将数据转换为一个个参数值和一条条测试用例方法,每一个参数值对应一条测试用例方法,其中测试用例名为“{测试用例方法名}_{参数值在数据中的序号}_{参数值}”;然后将参数值传入测试用例方法并执行。
如果不使用 ddt 库,则 ddt_demo1.py 的测试用例脚本实质等同于如下的测试用例脚本,或者说下面的脚本是上面测试用例执行过程中的一个中间状态:
# ddt_demo2.py
import unittest
class TestDDTDemo(unittest.TestCase):
def __test_ddt_data(self, value):
print(value)
def test_ddt_data_1_江雨桐(self):
self.__test_ddt_data("江雨桐")
def test_ddt_data_2_叶知秋(self):
self.__test_ddt_data("叶知秋")
def test_ddt_data_3_柳如烟(self):
self.__test_ddt_data("柳如烟")
def test_ddt_data_4_顾清歌(self):
self.__test_ddt_data("顾清歌")
if __name__ == '__main__':
unittest.main(verbosity=2)
执行 ddt_demo2.py 脚本文件,结果如下:
PS E:\LearnProject> python .\ddt_demo2.py
test_ddt_data_1_江雨桐 (__main__.TestDDTDemo) ... 江雨桐
ok
test_ddt_data_2_叶知秋 (__main__.TestDDTDemo) ... 叶知秋
ok
test_ddt_data_3_柳如烟 (__main__.TestDDTDemo) ... 柳如烟
ok
test_ddt_data_4_顾清歌 (__main__.TestDDTDemo) ... 顾清歌
ok
----------------------------------------------------------------------
Ran 4 tests in 0.001s
OK
单从结果上来观察,ddt_demo1.py 和 ddt_demo2.py 两个脚本是等量的。ddt_demo1.py 只不过是使用了ddt库,写法更简洁一些,可以说是 ddt_demo2.py 脚本的一个简洁版、升级版的写法。
ddt 库源码分析
从上面示例脚本中可以看到,我们只使用了两个装饰器 @ddt 和 @data 就成功实现了参数化。下面我们对这两个装饰器进行源码刨析。
@data 源码分析
当我们运行测试用例首先会执行测试用例上的装饰器,即 @data。因此,我们先来分析 data 函数。
data 函数:
def data(*values):
return idata(values)
data 函数接收一个参数,并返回 idata 函数。我们接着再来查看 idata 函数:
DATA_ATTR = '%values'
INDEX_LEN = '%index_len'
def idata(iterable, index_len=None):
if index_len is None:
# Avoid consuming a one-time-use generator.
iterable = tuple(iterable)
index_len = len(str(len(iterable)))
def wrapper(func):
setattr(func, DATA_ATTR, iterable)
setattr(func, INDEX_LEN, index_len)
return func
return wrapper
idata 中用到了一个内置函数 setattr,它是将一个属性赋值给对象。这个函数接受三个参数:对象、属性名(字符串形式)和属性值。
例如有一个 person = Person("Alice") 实例,使用 setattr(person, 'age', 30) 操作就是给 person 动态添加一个 age=30 的属性,后续可使用 person.age 调用获取其值。
从代码中可以看到 idata 是一个装饰器,最终返回的是内置函数 wrapper,而 wrapper 只干了两件事,就是给被装饰的函数 func 添加了 DATA_ATTR(变量的值为 '%values') 和 INDEX_LEN (变量的值为'%index_len')两个属性,其中 DATA_ATTR 的值为装饰器传入的参数;
INDEX_LEN 为传入参数数量的位数数,例如参数数量是3,则位数就是1,参数数量是30,则位数就是2,参数数量是300,则位数就是3,这样做的目的是为了在设置测试用例名称”{测试用例方法名}_{参数序号}_{参数}“时,参数序号的位数保持一致,例如有300个参数,第一条用例名称就是{测试用例方法名}_001_{参数},最后一条用例名称就是{测试用例方法名}_300_{参数},参数序号始终为3位数。
再来看ddt_demo1.py脚本示例中的 @data("江雨桐", "叶知秋", "柳如烟", "顾清歌"),此处就是给 test_ddt_data 测试用例方法添加了 %values=('江雨桐', '叶知秋', '柳如烟', '顾清歌') 和 %index_len=1 的两个属性。
@ddt 源码分析
了解了 @data 的作用后,我们再来看 @ddt 的作用,它都做了些那些事情。先看一下源码:
def ddt(arg=None, **kwargs):
fmt_test_name = kwargs.get("testNameFormat", TestNameFormat.DEFAULT)
def wrapper(cls):
for name, func in list(cls.__dict__.items()):
if hasattr(func, DATA_ATTR):
index_len = getattr(func, INDEX_LEN)
for i, v in enumerate(getattr(func, DATA_ATTR)):
test_name = mk_test_name(
name,
getattr(v, "__name__", v),
i,
index_len,
fmt_test_name
)
test_data_docstring = _get_test_data_docstring(func, v)
if hasattr(func, UNPACK_ATTR):
pass
else:
add_test(cls, test_name, test_data_docstring, func, v)
delattr(cls, name)
elif hasattr(func, FILE_ATTR):
pass
return cls
# ``arg`` is the unittest's test class when decorating with ``@ddt`` while
# it is ``None`` when decorating a test class with ``@ddt(k=v)``.
return wrapper(arg) if inspect.isclass(arg) else wrapper
注:代码中 if hasattr(func, UNPACK_ATTR) 和 elif hasattr(func, FILE_ATTR)分别为 @unpack 和 @file_data 两个装饰器分支上的内容,不在本次分析范围内,故为了方便大家观看,分支代码在此使用 pass 做了处理。
先分析一下 ddt 函数结构,首先是设置变量 fmt_test_name,然后是一个内置函数 wrapper,最后将内置函数 wrapper 返回。
-
fmt_test_name
= kwargs.get("testNameFormat", TestNameFormat.DEFAULT) 是用来设置测试用例名称格式的。测试用例名称格式有TestNameFormat.DEFAULT 和 TestNameFormat.INDEX_ONLY两种,其中 DEFAULT 是默认值,设置后的用例名称格式为{测试用例方法名}_{参数序号}_{参数};INDEX_ONLY 是只显示参数序号,设置后的用例名称格式为{测试用例方法名}_{参数序号}。用法如下:
@ddt(testNameFormat=TestNameFormat.DEFAULT)
class TestDDTDemo(unittest.TestCase):
pass
@ddt(testNameFormat=TestNameFormat.INDEX_ONLY)
class TestDDTDemo(unittest.TestCase):
pass
2. 获取到用例名称格式后,程序会先执行返回语句wrapper(arg) if inspect.isclass(arg) else wrapper,当使用@ddt装饰器装饰一个测试类时,arg是被装饰的 unittest 测试类。
在这种情况下,arg不是None;当使用 @ddt(k=v) 装饰一个测试类时,arg是None。这种用法允许用户通过关键字参数来传递配置给 ddt 装饰器。示例如下:
@ddt
class TestDDTDemo1(unittest.TestCase):
pass
@ddt(testNameFormat=TestNameFormat.INDEX_ONLY)
class TestDDTDemo2(unittest.TestCase):
pass
@ddt 写法,代码执行到 “wrapper(arg) if inspect.isclass(arg) else wrapper” 时,其中的 arg 就为被装饰的类 TestDDTDemo1,最终将被装饰的类当作参数带入内置函数 wrapper(arg);@ddt(testNameFormat=TestNameFormat.INDEX_ONLY) 写法,代码执行到 “wrapper(arg) if inspect.isclass(arg) else wrapper” 时,其中的 arg 就为 None,最终返回的是内置函数 wrapper。
3. 然后进入内置函数 wrapper 执行其代码,经过一番操作后最终将被装饰的类返回。
首先使用了 for name, func in list(cls.__dict__.items()) 遍历被装饰类的所有属性和方法(此处主要目的是获取测试类下的所有测试方法),接着进入了一个条件判断if hasattr(func, DATA_ATTR): pass elif hasattr(func, FILE_ATTR): pass 如果测试用例方法含有 DATA_ATTR 属性,则走 if 分支;如果测试用例方法含有 FILE_ATTR 属性,则走 elif 分支。
回顾 @data 源码分析 ,会动态给测试方法添加一个DATA_ATTR 属性,而 FILE_ATTR 属性也正是使用 @file_data 装饰器时动态添加的一个属性。因此,此处判断是逻辑是根据使用的装饰器来走不同的处理逻辑。由于我们使用的是 @data 装饰器,因此进入 if 判断逻辑。
4. if 判断逻辑下的第一行代码是 index_len = getattr(func, INDEX_LEN) ,获取传入参数数量的位数数。
5. 接着就是 for i, v in enumerate(getattr(func, DATA_ATTR)) 语句,遍历所有的参数,变量 i 为当前参数索引,变量 v 为当前参数。
for 循环语句下一共走了三个步骤:test_name = mk_test_name()、 test_data_docstring = _get_test_data_docstring() 以及一个判断语句 if hasattr(func, UNPACK_ATTR)。
if 判断语句判断的是测试用例方法是否有属性 UNPACK_ATTR,即判断是否使用了 @unpack 装饰器,本文示例不曾使用 @unpack 装饰器,也不在本次讲解范围内,故代码运行的是 else 分支,执行了一个 add_test() 函数。
故 for 循环下实际上依次调用了mk_test_name()、_get_test_data_docstring() 和 add_test() 三个函数。
6. mk_test_name()
mk_test_name() 是用来设置测试用例名称的一个函数,源码如下:
def mk_test_name(name, value, index=0, index_len=5, name_fmt=TestNameFormat.DEFAULT):
# Add zeros before index to keep order
index = "{0:0{1}}".format(index + 1, index_len)
if name_fmt is TestNameFormat.INDEX_ONLY or not is_trivial(value):
return "{0}_{1}".format(name, index)
try:
value = str(value)
except UnicodeEncodeError:
# fallback for python2
value = value.encode('ascii', 'backslashreplace')
test_name = "{0}_{1}_{2}".format(name, index, value)
return re.sub(r'\W|^(?=\d)', '_', test_name)
mk_test_name 函数很简单,就是根据给出的 name_fmt 值返回一个测试用例用例名称。
如果 name_fmt=TestNameFormat.INDEX_ONLY,
则返回 "{0}_{1}".format(name, index);反之返回 "{0}_{1}_{2}".format(name, index, value)。
其中 name 为被装饰的测试用例方法/函数名称,index 为”索引+1“值,value 就是对应的参数,之间使用下划线连接。故最终的测试用例名称为 {测试用例方法名}_{参数序号}_{参数} 或 {测试用例方法名}_{参数序号}。
最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】


被折叠的 条评论
为什么被折叠?



