ddt模块使用:
import unittest
import ddt
import os
cases = [
{'title': '用例1', 'data': 1111},
{'title': '用例2', 'data': 2222},
{'title': '用例2', 'data': 3333}
]
@ddt.ddt
class TestLong(unittest.TestCase):
@ddt.data(*cases)
def test1(self, case):
...
print(case)
if __name__ == '__main__':
suite = unittest.defaultTestLoader.discover(os.path.dirname(os.path.abspath(__file__)))
runner = unittest.TextTestRunner()
runner.run(suite)
ddt模块运用了两个装饰器实现用例参数化。现在不用这个模块,同样用两个装饰器达到用例参数化的目的执行测试用例!!
第一步
根据测试用例条数,动态的给测试类添加方法,有多少条用例,就执行多少次测试方法:
cases = [
{'title': '用例1', 'data': 1111},
{'title': '用例2', 'data': 2222},
{'title': '用例2', 'data': 3333}
]
class TestDemo:
def test_login(self):
pass
目的:编写一个装饰器用来装饰测试类,根据测试数据动态的添加测试方法,现在cases有三条用例,需要添加:test_login_0、test_login_1、test_login_2三个方法。
思路:定义一个类装饰器,返回类对象,遍历测试数据,根据已有的测试方法+用例下标命名,利用内置函数setattr给测试类添加方法
注意:
- 非闭包形式的装饰器,仅修改被装饰对象的属性,不做功能上的扩展,不调用函数。
- setattr 给对象赋值属性,值可以是任意类型,比如是一个可调用的方法对象:<function TestDemo.test_login at 0x01F3AC88>
rom pprint import pprint
cases = [
{'title': '用例1', 'data': 1111},
{'title': '用例2', 'data': 2222},
{'title': '用例2', 'data': 3333}
]
# 非闭包装饰器,仅修改被装饰的对象的属性,不调用被装饰对象,不做功能上的扩展
def parameter(cls):
for index,case in enumerate(cases):
new_method_name = f'test_login_{index}'
setattr(cls,new_method_name,cls.test_login) # 给类添加测试方法
return cls
@parameter
class TestDemo:
def test_login(self):
pass
# 打印测试的属性字典
pprint(TestDemo.__dict__)
# 输出
mappingproxy({'__dict__': <attribute '__dict__' of 'TestDemo' objects>,
'__doc__': None,
'__module__': '__main__',
'__weakref__': <attribute '__weakref__' of 'TestDemo' objects>,
'test_login': <function TestDemo.test_login at 0x01F3AC88>,
'test_login_0': <function TestDemo.test_login at 0x01F3AC88>,
'test_login_1': <function TestDemo.test_login at 0x01F3AC88>,
'test_login_2': <function TestDemo.test_login at 0x01F3AC88>})
由此可以给测试类动态的添加测试方法,这些方法其实就是原本的测试方法test_login。在python解释器加载类时,会执行装饰器,给测试类添加这些方法,但不会运行。只有在unittest执行用例时,这些方法test_login、test_login_0、test_login_1、test_login_2才会被执行。
第二步
动态的获取测试类中的方法名,测试类中原本有几个测试方法,就根据测试数据动态的给类添加方法。
目的:上面的步骤,由于这里是固定的写法:new_method_name = f'test_login_{index}',只能添加test_login方法,如果我还有其他的测试方法,如test_register等,如何根据测试数据动态的添加test_register_0、test_register_1、test_register_2、test_register_3方法?如果有些用例并不需要用到测试数据不需要添加方法?
思路:定义一个测试方法的装饰器,装饰测试方法,给需要用到测试数据的方法添加一个属性,遍历测试类的属性字典,当这个方法拥有这个属性时,即代表他需要添加这个测试类的方法。
注意:
- for method_name,obj in attr_list: 在这一步之前,必须将cls.__dict__.items()转换成列表,不能直接循环一个cls.__dict__.items()字典的items对象,在下面的循环中会给cls添加属性,所以cls的属性字典会一直变化,for循环不能循环一个一直在变化的字典的items对象。将属性字典的items对象转换成列表以后才能继续循环。
from pprint import pprint
cases = [
{'title': '用例1', 'data': 1111},
{'title': '用例2', 'data': 2222},
{'title': '用例2', 'data': 3333}
]
# 非闭包装饰器,返回被装饰的对象:给类添加可调用属性,即方法
def parameter(cls):
attr_list = list(cls.__dict__.items()) # 这里必须将对象的属性字典的items转换为列表,因为下面的循环
for method_name,obj in attr_list:
if hasattr(obj,'is_test'):
new_method = obj # 将方法对象赋值新的变量
for index,case in enumerate(cases):
new_method_name = f'{method_name}_{index}'
setattr(cls,new_method_name,new_method) # 给类添加测试方法
return cls
# 非闭包装饰器,返回被装饰的对象:给测试方法添加一个is_test属性
def mark(func):
func.is_test = True
return func
@parameter
class TestDemo:
@mark
def test_login(self):
pass
@mark
def test_register(self):
pass
# 打印测试的属性字典
pprint(TestDemo.__dict__)
# 输出:
mappingproxy({'__dict__': <attribute '__dict__' of 'TestDemo' objects>,
'__doc__': None,
'__module__': '__main__',
'__weakref__': <attribute '__weakref__' of 'TestDemo' objects>,
'test_login': <function TestDemo.test_login at 0x01585CD0>,
'test_login_0': <function TestDemo.test_login at 0x01585CD0>,
'test_login_1': <function TestDemo.test_login at 0x01585CD0>,
'test_login_2': <function TestDemo.test_login at 0x01585CD0>,
'test_register': <function TestDemo.test_register at 0x01585D18>,
'test_register_0': <function TestDemo.test_register at 0x01585D18>,
'test_register_1': <function TestDemo.test_register at 0x01585D18>,
'test_register_2': <function TestDemo.test_register at 0x01585D18>})
第三步
对于每一个测试方法,测试数据都是一样的,而且只是新增了执行测试用例的方法,如果每个测试方法都适用不同的测试数据要怎么实现?如何在测试方法中调用数据且能通过unittest执行用例?
目的:针对不同的测试数据,新增测试方法,在方法中调用用例数据
思路:通过装饰器,给测试方法新增一个属性,属性值就是测试数据
from pprint import pprint
import unittest
import os
# 非闭包装饰器,返回被装饰的对象:给类添加可调用属性,即方法
def parameter(cls):
attr_list = list(cls.__dict__.items()) # 这里必须将对象的属性字典的items转换为列表,因为下面的循环
for method_name,obj in attr_list:
if hasattr(obj,'is_test'):
cases = obj.params # 将方法的params属性赋值给cases
new_method = obj # 将方法对象赋值新的变量
for index,case in enumerate(cases):
# 重新定义一个函数作为测试方法
def new_test_method(self):
new_method(self,case)
new_method_name = f'{method_name}_{index}'
setattr(cls,new_method_name,new_test_method) # 给类添加测试方法
return cls
# 外层函数接收一个参数datas,并设置函数的属性值为datas
def data(datas):
def mark(func):
func.is_test = True
func.params = datas
return func
return mark
@parameter
class TestDemo(unittest.TestCase):
cases1 = [
{'title': 'test_login用例1', 'data': 1111},
{'title': 'test_login用例2', 'data': 2222},
{'title': 'test_login用例2', 'data': 3333}
]
cases2 = [
{'title': 'test_register用例1', 'data': 1111},
{'title': 'test_register用例2', 'data': 2222},
]
@data(cases2)
def test_login(self,data):
print(data)
@data(cases2)
def test_register(self,data):
print(data)
if __name__ == '__main__':
suite = unittest.defaultTestLoader.discover(os.path.dirname(os.path.abspath(__file__)),pattern='test自主编写ddt模块.py')
TestRunner(suite).run()
pprint(TestDemo.__dict__)
注意:代码中的这一步new_method = obj、new_method(self,case)必不可少,而不能直接用obj(self,case)。python解释器在解析类和装饰器时,直接用obj(self,case)并不会报错,也成功给类添加了方法:<function parameter.<locals>.new_test_method at 0x04064F58>等等。但是在unittest执行测试用例时:比如执行test_login_1,就是在执行new_test_method方法,会调用obj(self,case),但是解释器在运行最后一个for循环时,obj这个临时变量=None(或者其他)并不是一个可调用对象了,此时就会报错。同理new_method = obj也只是被赋值了test_register这个对象,我们在执行这些用例的时候,其实都是在执行test_register这个测试方法。第四步再进行优化
在运行unittest执行用例前,python解释器通过装饰器动态的给类添加了可调用的属性new_test_method。只有unittest运行用例时如test_login_1、test_register_0等等时才真正调用了new_test_method这个方法对象。
最终运行的结果:
可以发现最终这个两个原始的方法:test_login、test_register报错了,不存在data参数。
总的来说,就是test_login_0、test_login_1等等方法在执行时就是在执行new_test_method,所以形参 data接收了实参case:{'title': 'test_register用例2', 'data': 2222}。而原来的两个方法并没有并实参传递,所以会报错。
所以最后,需要删除原来的两个方法:delattr(cls,method_name)
# 非闭包装饰器,返回被装饰的对象:给类添加可调用属性,即方法
def parameter(cls):
attr_list = list(cls.__dict__.items()) # 这里必须将对象的属性字典的items转换为列表,因为下面的循环
for method_name,obj in attr_list:
if hasattr(obj,'is_test'):
cases = obj.params # 将方法的params属性赋值给cases
new_method = obj # 将方法对象赋值新的变量
for index,case in enumerate(cases):
# 重新定义一个函数作为测试方法
def new_test_method(self):
new_method(self,case)
new_method_name = f'{method_name}_{index}'
setattr(cls,new_method_name,new_test_method) # 给类添加测试方法
else:
delattr(cls,method_name)
return cl
第四步
从上一步可以知道,new_method = obj 和 case这两个变量,在python解释器运行完成以后,他们的值就是for循环遍历完成以后得到的值,就是说new_method这个变量永远都是test_register这个方法对象,case这个变量永远都是测试数据case2的最后一项(这两个的值都是for遍历最后一次的值)
举一个简单的栗子:
li = []
for i in range(5):
li.append(lambda:i)
print(li)
print(f'临时变量i为:{i},传入lambda函数参数为{i}')
for x in li2:
print(x())
目的:new_method和case这两个变量怎么才能数据锁定:new_method和case不再是被赋值for遍历完成后的值
思路:利用闭包的封闭作用域,在每次循环下,对new_method和case两个变量进行数据锁定
对以上栗子进行数据锁定
li = []
for i in range(5):
def func(i):
wrapper = lambda:i
return wrapper
li.append(func(i))
print(li)
for x in li:
print(x())
这个时候,每一个传入的i值都被封闭作用域进行数据锁定了
同理,应用于parameter装饰器,进行优化:
# 非闭包装饰器,返回被装饰的对象:给类添加可调用属性,即方法
def parameter(cls):
attr_list = list(cls.__dict__.items()) # 这里必须将对象的属性字典的items转换为列表,因为下面的循环
for method_name,obj in attr_list:
if hasattr(obj,'is_test'):
cases = getattr(obj,'params') # 将方法的params属性赋值给cases
new_method = obj # 将方法对象赋值新的变量
for index,case in enumerate(cases):
# 闭包,封闭作用域对method,data进行数据锁定
def closure(method,data):
def new_test_method(self):
method(self,data) # 引用外部非全局变量method、data
return new_test_method
new_method_obj = closure(new_method,case)
new_method_name = f'{method_name}_{index}'
setattr(cls,new_method_name,new_method_obj) # 给类添加测试方法
else:
delattr(cls,method_name)
return cls
最终运行结果:
最后进行一下封装,将这两个装饰器放在一个模块中,所有的用例都可以用这两个装饰器进行用例参数化:
# myselfddt.py
# 非闭包装饰器,返回被装饰的对象:给类添加可调用属性,即方法
def parameter(cls):
attr_list = list(cls.__dict__.items()) # 这里必须将对象的属性字典的items转换为列表,因为下面的循环
for method_name,obj in attr_list:
if hasattr(obj,'is_test'):
cases = getattr(obj,'params') # 将方法的params属性赋值给cases
new_method = obj # 将方法对象赋值新的变量
for index,case in enumerate(cases):
# 闭包,封闭作用域对method,data进行数据锁定
def closure(method,data):
def new_test_method(self):
method(self,data) # 引用外部非全局变量method、data
return new_test_method
new_method_obj = closure(new_method,case)
new_method_name = f'{method_name}_{index}'
setattr(cls,new_method_name,new_method_obj) # 给类添加测试方法
else:
delattr(cls,method_name)
return cls
# 外层函数接收一个参数datas,并设置函数的属性值为datas
def data(datas:list or tuple or str):
def mark(func):
func.is_test = True
func.params = datas
return func
return mark
test_demo.py
import unittest
import os
from myselfddt import parameter,data
@parameter
class TestMyDdt(unittest.TestCase):
cases = [lambda:x for x in range(4)]
@data([1,2,3,4])
def testdemo1(self,case):
print(case)
@data(('a','b','c'))
def testdemo2(self,case):
print(case)
@data('def')
def testdemo3(self,case):
print(case)
@data(cases)
def testdemo4(self,case):
print(case)
if __name__ == '__main__':
suite = unittest.defaultTestLoader.discover(os.path.dirname(os.path.abspath(__file__)),pattern='test_demo.py')
TestRunner(suite).run()
报告如下: