自主实现DDT用例参数化装饰器

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()

 报告如下:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值