一:ddt 数据驱动/参数化

1.什么是ddt

痛点:测试代码和测试数据耦合了,修改测试或者增加测试都需要修改代码。

DDT:DATA DRIVER TEST (数据驱动测试/参数化),是一个设计思想,不同的人有不同的理解。 它解决的问题:测试数据与测试用例代码分离,通过外部数据生成单元测试函数。

2.ddt 模块

pip install ddt

ddt模块的原理:本质上就是一个装饰器,总共高就百来行代码。在创建类的时候,根据用例数据,动态的创建测试函数。

使用

  1. ddt模块中导入from ddt import ddt, data两个装饰器

  2. 定义测试用例类时需要用

    @ddt
    class SomeTestCase(unittest.TestCase):
     pass
    
  3. 定义单元测试函数时,需要

    @data(case1, case2, case3,...)
    def test_some(self, case):
     pass
    
  4. ddt模块会自动的通过case1,case2,...在类里面创建 test_some_1,test_some_2,..单元测试函数

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/11/8 21:08
# @Author  : shisuiyi
# @File    : test_login.py
# @Software: win10 Tensorflow1.13.1 python3.9
import unittest
from testcode import login
from ddt import ddt, data
# from unittestreport import ddt, list_data
cases = [
    {"title": "登陆成功", "request": {"username": "python45", "password": "lemonban"},
     "expect": {"code": 0, "msg": "登录成功"}},
    {"title": "账号或密码不正确", "request": {"username": "python45", "password": "lemonban123"},
     "expect": {"code": 1, "msg": "账号或密码不正确"}},
    {"title": "账号或密码不正确", "request": {"username": "python45123", "password": "lemonban"},
     "expect": {"code": 1, "msg": "账号或密码不正确"}},
    {"title": "密码长度在6-18位之间", "request": {"username": "python45", "password": "lemo"},
     "expect": {"code": 1, "msg": "密码长度在6-18位之间"}},
    {"title": "密码长度在6-18位之间", "request": {"username": "python45", "password": "lemonbanlemonbanlemonbanlemonbanlemonban"},
     "expect": {"code": 1, "msg": "密码长度在6-18位之间"}},
]


@ddt
class TestLogin(unittest.TestCase):
    """
    测试登录函数
    """

    @classmethod
    def setUpClass(cls) -> None:
        """
        类级前置
        :return:
        """
        print('我是类级前置,在整个类的测试前执行')

    @classmethod
    def tearDownClass(cls) -> None:
        """
        类级后置
        :return:
        """
        print('我是类级后置,在整个测试类测试完成后执行')

    def setUp(self) -> None:
        """
        方法级前置
        :return:
        """
        print('======我会在每个测试执行前执行====')

    def tearDown(self) -> None:
        """
        方法级后置
        :return:
        """
        print('======我会在每个测试执行后执行======')

    @data(*cases)
    # @list_data(cases)
    def test_login(self, case):
        
        # 1. 准备测试数据
        print(case)
        print('{}开始测试'.format(case['title']))
        # 2. 测试步骤
        res = login(**case['request'])
        # 3. 断言
        self.assertEqual(res, case['expect'])


if __name__ == '__main__':
    unittest.main()

输出

Launching unittests with arguments python -m unittest D:/Lemon/py45/day17/test_ddt_login.py in D:\Lemon\py45\day17

我是类级前置,在整个类的测试前执行
======我会在每个测试执行前执行====
{'title': '登陆成功', 'request': {'username': 'python45', 'password': 'lemonban'}, 'expect': {'code': 0, 'msg': '登录成功'}}
登陆成功开始测试
======我会在每个测试执行后执行======
======我会在每个测试执行前执行====
{'title': '账号或密码不正确', 'request': {'username': 'python45', 'password': 'lemonban123'}, 'expect': {'code': 1, 'msg': '账号或密码不正确'}}
账号或密码不正确开始测试
======我会在每个测试执行后执行======
======我会在每个测试执行前执行====
{'title': '账号或密码不正确', 'request': {'username': 'python45123', 'password': 'lemonban'}, 'expect': {'code': 1, 'msg': '账号或密码不正确'}}
账号或密码不正确开始测试
======我会在每个测试执行后执行======
======我会在每个测试执行前执行====
{'title': '密码长度在6-18位之间', 'request': {'username': 'python45', 'password': 'lemo'}, 'expect': {'code': 1, 'msg': '密码长度在6-18位之间'}}
密码长度在6-18位之间开始测试
======我会在每个测试执行后执行======
======我会在每个测试执行前执行====
{'title': '密码长度在6-18位之间', 'request': {'username': 'python45', 'password': 'lemonbanlemonbanlemonbanlemonbanlemonban'}, 'expect': {'code': 1, 'msg': '密码长度在6-18位之间'}}
密码长度在6-18位之间开始测试
======我会在每个测试执行后执行======
我是类级后置,在整个测试类测试完成后执行


Ran 5 tests in 0.010s

OK

Process finished with exit code 0

ddt-data 数据

一组数据之间用逗号隔开
data的参数可以为如下几种

  • 1.一组数据中,每个数据为单个值
  • 2.一组数据中,每个数据为一个列表或者一个字典
  • 3.文件对象:json,yaml

一组数据中的数据为列表或者字典:
@data([a,b],[c,d])

如何从以上数据中获取到字典中每一项值
@unpack
若变量A=[{a:b,a1:b1},{c:d,c1:d1}]
如何将变量A中的每一组元素作为测试数据
@ddt.data(*A)

包含知识点

@unpack :当传递的是复杂的数据结构时使用。比如使用元组或者列表,添加 @unpack 之后, ddt 会自动把元组或者列表对应到多个参数上。字典也可以这样处理

  • 当没有加unpack时,test_case方法的参数只能填一个;
  • 当你加了unpack时,传递的数据量需要一致;如列表例子中,每个列表我都固定传了2个数据,当你多传或少传时会报错,而test_case方法的参数也要写2个,需要匹配上
  • 当传的数据是字典类型时,要注意每个字典的key都要一致,test_case的参数的命名也要一致;如字典的例子,两个字典的key都是value1和value2,而方法的参数也是
  • 当传的数据是通过变量的方式,变量前需要加上*

列表

import ddt
import unittest

@ddt.ddt
class test_PersonInfo(unittest.TestCase):
    @classmethod
    def setUp(self):
        print("==========开始测试==========")
    @classmethod
    def tearDown(self):
        print("==========结束测试==========")
 
    @ddt.data([1,2],[3,4])
    def test_print(self,a):
        print(a)

if __name__ == '__main__':
    unittest.main()
=====================输出结果====================

[1, 2]

[3, 4]

print(a[0])

1
3

列表拆包

    @ddt.data([1,2],[3,4])
    @ddt.unpack
    def test_print(self,a,b):
        print(a,b)
 
==========开始测试==========
1 2
==========结束测试==========
==========开始测试==========
3 4
==========结束测试==========

字典

@data({'value1': 1, 'value2': 2}, {'value1': 1, 'value2': 2})
    @unpack
    def test_dict(self, value1, value2):
        print("test_dict", value1, value2)

===============输出==============
test_dict 1 2
test_dict 1 2

元祖

tuples = ((1, 2, 3), (1, 2, 3))
 @data(*tuples)
    def test_tuples(self, n):
        print("test_tuples", n)
=============输出================
test_tuples (1, 2, 3)
test_tuples (1, 2, 3)

包含字典的列表

datas = [{'class': 'python2', 'name': 'xiaoshitou', 'id': '0001', 'sex': 'male'},
         {'class': 'python2', 'name': 'nuonuo', 'id': '0002', 'sex': 'female'},
         {'class': 'python2', 'name': 'fly', 'id': '0003', 'sex': 'male'},
         {'class': 'python2', 'name': 'haiyang', 'id': '0004', 'sex': 'male'},
         {'class': 'python2', 'name': 'shuangshuang', 'id': '0005', 'sex': 'female'}
]
@ddt.data(*datas)
    def test_print(self,datas):
        print(datas)

if __name__ == '__main__':
            unittest.main()

            
==========开始测试==========
{'class': 'python2', 'name': 'xiaoshitou', 'id': '0001', 'sex': 'male'}
==========结束测试==========
==========开始测试==========
{'class': 'python2', 'name': 'nuonuo', 'id': '0002', 'sex': 'female'}
==========结束测试==========
==========开始测试==========
{'class': 'python2', 'name': 'fly', 'id': '0003', 'sex': 'male'}
==========结束测试==========
==========开始测试==========
{'class': 'python2', 'name': 'haiyang', 'id': '0004', 'sex': 'male'}
==========结束测试==========
==========开始测试==========
{'class': 'python2', 'name': 'shuangshuang', 'id': '0005', 'sex': 'female'}
==========结束测试==========

传递json文件

dat-data数据
数据为文件对象:Json yaml
@ddt.file_data(jason文件路径) json格式里面必须为双引号“”,且必须为键值对,不存在纯列表格式,值作为测试数据
{"name":"nick","gender":"male","age":29}

JSON文件

{
  "first": [
    {
      "isRememberMe": "True",
      "password": "111111",
      "username": "root"
    },
    "200"
  ],
  "second": [
    "{'isRememberMe': True, 'password': '1111111', 'username': 'root'}",
    "406"
  ],
  "third": [
    1,
    2
  ],
  "four": "123123"
}

单元测试类

from ddt import *


# 在测试类前必须首先声明使用 ddt
@ddt
class imoocTest(unittest.TestCase):

    @file_data('F:/test/config/testddt.json')
    def test_json(self, data):
        print(data)
 
==========输出结果==========
[{'isRememberMe': 'True', 'password': '111111', 'username': 'root'}, '200']
["{'isRememberMe': True, 'password': '1111111', 'username': 'root'}", '406']
[1, 2]
123123
      

传递YAML文件

YAML文件

unsorted_list:
  - 10
  - 15
  - 12

sorted_list: [ 15, 12, 50 ]

单元测试类

from ddt import *


# 在测试类前必须首先声明使用 ddt
@ddt
class imoocTest(unittest.TestCase):

    @file_data('F:/test/config/testddt.yaml')
    def test4(self, data):
        print("yaml", data)
===============输出结果==================
yaml [10, 15, 12]
yaml [15, 12, 50]

3. unittestreport

这个也有ddt功能,使用方法同ddt

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/11/8 21:08
# @Author  : shisuiyi
# @File    : test_login.py
# @Software: win10 Tensorflow1.13.1 python3.9
import unittest
from testcode import login
# from ddt import ddt, data
from unittestreport import ddt, list_data
cases = [
    {"title": "登陆成功", "request": {"username": "python45", "password": "lemonban"},
     "expect": {"code": 0, "msg": "登录成功"}},
    {"title": "账号或密码不正确", "request": {"username": "python45", "password": "lemonban123"},
     "expect": {"code": 1, "msg": "账号或密码不正确"}},
    {"title": "账号或密码不正确", "request": {"username": "python45123", "password": "lemonban"},
     "expect": {"code": 1, "msg": "账号或密码不正确"}},
    {"title": "密码长度在6-18位之间", "request": {"username": "python45", "password": "lemo"},
     "expect": {"code": 1, "msg": "密码长度在6-18位之间"}},
    {"title": "密码长度在6-18位之间", "request": {"username": "python45", "password": "lemonbanlemonbanlemonbanlemonbanlemonban"},
     "expect": {"code": 1, "msg": "密码长度在6-18位之间"}},
]


@ddt
class TestLogin(unittest.TestCase):
    """
    测试登录函数
    """

    @classmethod
    def setUpClass(cls) -> None:
        """
        类级前置
        :return:
        """
        print('我是类级前置,在整个类的测试前执行')

    @classmethod
    def tearDownClass(cls) -> None:
        """
        类级后置
        :return:
        """
        print('我是类级后置,在整个测试类测试完成后执行')

    def setUp(self) -> None:
        """
        方法级前置
        :return:
        """
        print('======我会在每个测试执行前执行====')

    def tearDown(self) -> None:
        """
        方法级后置
        :return:
        """
        print('======我会在每个测试执行后执行======')

    # @data(*cases)
    @list_data(cases)
    def test_login(self, case):
        
        # 1. 准备测试数据
        print(case)
        print('{}开始测试'.format(case['title']))
        # 2. 测试步骤
        res = login(**case['request'])
        # 3. 断言
        self.assertEqual(res, case['expect'])


if __name__ == '__main__':
    unittest.main()

二:excel用例文件处理

1. openpyxl 模块

读写excel文件的第三方库 支持的格式: excel 2010 xlsx/xlsm/xltx/xltm

pip install openpyxl

基本操作

示例图表:

# 1. 打开工作簿
import openpyxl
wb = openpyxl.load_workbook('testdata.xlsx') # 读取excel文件返回一个workbook对象
print(wb, type(wb))

# 输出
<openpyxl.workbook.workbook.Workbook object at 0x000002476C754BB0> <class 'openpyxl.workbook.workbook.Workbook'>
# 2. 读取表
# 一个excel文件是要给workbook,拥有多个表,通过sheetname获取对应的表
ws = wb['login'] # 获取login表
print(ws, type(ws))
# 通过表名可以以字典取值的方式获取对应的表。封装为一个worksheet对象

# 输出
<Worksheet "login"> <class 'openpyxl.worksheet.worksheet.Worksheet'>
wb.active  # workbook对象的active属性返回第一个表

输出

<Worksheet "login">
# 3. 读取单元格
# 通过表的行和列指定单元格,注意行号,列号以1开始cell = ws.cell(row=1,column=1)  
# 单元格封装为cell对象print(cell, type(cell))
# 输出<Cell 'login'.A1> <class 'openpyxl.cell.cell.Cell'>
# 通过cell对象的value属性可以取出单元格中的值cell.value

输出

'id'
# 4. 读取一行
# worksheet对象的rows属性返回表的行数据的迭代对象
ws.rows

输出

<generator object Worksheet._cells_by_row at 0x000002476E52D5F0>
for row in ws.rows:
    for item in row:
        print(item.value, end=' ')
    print()
    
    
# 输出
id title request expect 
1 登录成功 {"username": "python45", "password": "lemonban"} {"code": 0, "msg": "登录成功"} 
None 密码错误 {"username": "python45", "password": "lemonban1"} {"code": 1, "msg": "账号或密码不正确"} 
None 账号错误 {"username": "python415", "password": "lemonban"} {"code": 1, "msg": "账号或密码不正确"} 
None 密码过短 {"username": "python45", "password": "lemon"} {"code": 1, "msg": "密码长度在6-18位之间"} 
None 密码过长 {"username": "python45", "password": "lemonban123456789012345678"} {"code": 1, "msg": "密码长度在6-18位之间"} 

2. 封装读取excel用例数据的功能

是封装成一个函数,还是封装成一个类。

在大多数情况下,简单功能(代码函数较少,逻辑较简单)可以直接封装成函数。

复杂功能,数据结构复杂,一般进行面向对象封装。

1. 功能分析

  1. 根据exel文件名读取对应的文件数据
  2. 根据工作表名读取对应表数据
  3. 返回字典列表格式便于处理

2.封装成函数

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/11/11 20:51
# @Author  : shisuiyi
# @File    : read_excel_tool.py
# @Software: win10 Tensorflow1.13.1 python3.9
# 测试数据处理模块

from openpyxl import load_workbook


def get_data_from_excel(file, sheet_name=None):
    """
    获取excel文件中指定表里的测试数据
    :param file:
    :param sheet_name:
    """

    # 1. 读取excel文件 
    wb = load_workbook(file)  # 读取excel文件返回一个workbook对象
    # 2. 读取对应的表
    if sheet_name is None:
        # 如果不传sheet_name,默认使用第一张表
        ws = wb.active
    else:
        ws = wb[sheet_name] # 通过表名可以以字典取值的方式获取对应的表。封装为一个worksheet对象
    # 3. 创建一个列表容器存放数据
    data = []
    # 4. 组织数据
    # 4.1 获取表头,作为字典的key
    row_list = list(ws.rows)   # worksheet对象的rows属性返回表的行数据的迭代对象
    # tilte = []
    # # 拿到第一行,然后迭代
    # for key in row_list[0]:
    #     tilte.append(key.value)  # 通过cell对象的value属性可以取出单元格中的值
    title = [item.value for item in row_list[0]]  # 列表生成式
    # 4.2 获取其他数据
    for row in row_list[1:]:
        # 获取每一行的数据
        temp = [i.value for i in row]
        # 将表头与这一行数据打包,换成字典
        data.append(dict(zip(title, temp)))
    return data


if __name__ == '__main__':
    res = get_data_from_excel('testdata.xlsx')
    print(res)

使用封装的函数读取excel中的元素然后进行测试

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/11/8 21:08
# @Author  : shisuiyi
# @File    : test_login.py
# @Software: win10 Tensorflow1.13.1 python3.9
import unittest
from testcode import login
# from ddt import ddt, data
from unittestreport import ddt, list_data
from read_excel_tool import get_data_from_excel

# cases = [
#     {"title": "登陆成功", "request": {"username": "python45", "password": "lemonban"},
#      "expect": {"code": 0, "msg": "登录成功"}},
#     {"title": "账号或密码不正确", "request": {"username": "python45", "password": "lemonban123"},
#      "expect": {"code": 1, "msg": "账号或密码不正确"}},
#     {"title": "账号或密码不正确", "request": {"username": "python45123", "password": "lemonban"},
#      "expect": {"code": 1, "msg": "账号或密码不正确"}},
#     {"title": "密码长度在6-18位之间", "request": {"username": "python45", "password": "lemo"},
#      "expect": {"code": 1, "msg": "密码长度在6-18位之间"}},
#     {"title": "密码长度在6-18位之间", "request": {"username": "python45",
#     "password": "lemonbanlemonbanlemonbanlemonbanlemonban"},
#      "expect": {"code": 1, "msg": "密码长度在6-18位之间"}},
# ]

cases = get_data_from_excel('testdata.xlsx', 'login')


@ddt
class TestLogin(unittest.TestCase):
    """
    测试登录函数
    """

    @classmethod
    def setUpClass(cls) -> None:
        """
        类级前置
        :return:
        """
        print('我是类级前置,在整个类的测试前执行')

    @classmethod
    def tearDownClass(cls) -> None:
        """
        类级后置
        :return:
        """
        print('我是类级后置,在整个测试类测试完成后执行')

    def setUp(self) -> None:
        """
        方法级前置
        :return:
        """
        print('======我会在每个测试执行前执行====')

    def tearDown(self) -> None:
        """
        方法级后置
        :return:
        """
        print('======我会在每个测试执行后执行======')

    # @data(*cases)
    @list_data(cases)
    def test_login(self, case):
        # 1. 准备测试数据
        print(case)
        print('{}开始测试'.format(case['title']))
        # 2. 测试步骤
        res = login(**eval(case['request']))
        # 3. 断言
        self.assertEqual(res, eval(case['expect']))


if __name__ == '__main__':
    unittest.main()