unittest— 单元测试框架

​ Unittest测试框架的的原理是将继承unittest.TestCase的测试类中,所有的test开头的测试函数,生成该测试类的一个对象,然后组装成测试套件,使用测试运行器(TestRunner)运行并使用测试结果(TestResult)对象纪录每个用例的运行状态。

基本架构

  • TestCase :测试用例
  • TestSuite:测试套件
  • TestLoader:测试用例加载器
  • TestResult:测试结果记录器
  • TestRunner:测试运行器

测试用例 (TestCase)

用例文件创建

用例执行顺序:并非按书写顺序执行,而是按用例名ascii码先后顺序执行

  1. 导入unittest模块
  2. 创建测试类,并继承unittest.TestCase
  3. 定义测试函数,函数名以test开头

用例类定义

  • 创建的类必须继承 unitest.TestCase

    1
    2
    import unittest
    class TestDemo(unittest.TestCase)

用例方法定义

  • 每一个测试用例都是一个方法, 以 test_ 开头

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import unittest
    class TestDemo(unittest.TestCase):
    def test_01_login(self):
    print("login case")

    def test_02_logout(self):
    print("logout case")

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

命令行界面执行当前测试类的用例

1
2
3
4
5
6
7
8
# 以文件执行
python -m unittest test_module1 test_module2
# 以类为单位执行
python -m unittest test_module.TestClass
# 以方法为单位执行
python -m unittest test_module.TestClass.test_method
# 可以通过传入 -v 标志来运行更详细(更详细)的测试:
python -m unittest -v test_module

断言

方法 检查 新品
assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a, b) a is b 3.1
assertIsNot(a, b) a is not b 3.1
assertIsNone(x) x is None 3.1
assertIsNotNone(x) x is not None 3.1
assertIn(a, b) a in b 3.1
assertNotIn(a, b) a not in b 3.1
assertIsInstance(a, b) isinstance(a, b) 3.2
assertNotIsInstance(a, b) not isinstance(a, b) 3.2
assertGreater(a, b) a > b 3.1
assertGreaterEqual(a, b) a >= b 3.1
assertLess(a, b) a < b 3.1
assertLessEqual(a, b) a <= b 3.1

测试夹具 (FixTure)

测试夹具表示执行一项或多项测试夹具所需的准备工作 测试,以及任何关联的清理操作。例如,这可能涉及 创建临时或代理数据库、目录或启动服务器 过程。

setup()

​ 用例前置方法,每执行一个测试用例 (def) 之前都会调用一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import unittest
class TestDemo(unittest.TestCase):
# 每执行完一个def 测试用例之前就会执行一次setUp()
def setUp(self) -> None:
print("def init")

def test_01_login(self) -> None:
print("login case")

def test_02_logout(self) -> None:
print("logout case")

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

TearDown()

​ 用例后置方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import unittest
class TestDemo(unittest.TestCase):
def setUp(self) -> None:
print("def setup")
# 每执行完一个def 测试用例之后就会执行一次tearDown()
def tearDown(self) -> None:
print("def tearDown")

def test_01_login(self) -> None:
print("login case")

def test_02_logout(self) -> None:
print("logout case")

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

setUpClass()

​ 类前置方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import unittest
class TestDemo(unittest.TestCase):
# 每执行类之前就会执行一次setUpClass()
def setUpClass(cls) -> None:
print("class setUp")

def setUp(self) -> None:
print("def setup")

def tearDown(self) -> None:
print("def tearDown")

def test_01_login(self) -> None:
print("login case")

def test_02_logout(self) -> None:
print("logout case")

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

TearDownClass()

​ 类后置方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import unittest
class TestDemo(unittest.TestCase):
def setUpClass(cls) -> None:
print("class setUp")
# 每执行类之后就会执行一次tearDownClass()
def tearDownClass(cls) -> None:
print("class tearDown")

def setUp(self) -> None:
print("def setup")

def tearDown(self) -> None:
print("def tearDown")

def test_01_login(self) -> None:
print("login case")

def test_02_logout(self) -> None:
print("logout case")

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

测试套件 (TestSuite)

测试套件是测试用例和/或测试套件的集合。收集测试用例

​ 在正常的命令执行时,一次只能运行一个文件或一个测试用例;使用测试套件就可以将一些python文件放在一起执行

创建测试套件

1
2
3
4
import unittest

# 创建测试套件
suite = unittest.TestSuite()

创建用例加载器 - 装载器

1
2
3
4
5
6
7
8
import unittest

# 创建测试套件
suite = unittest.TestSuite()

# 创建用例加载器 - 往测试套件添加测试用例用的
loader = unittest.TestLoader()

添加用例

  • loadTestsFromTestCase(‘测试用例的类名’) - 通过测试类对象加载其中的所有测试函数,生成测试套件

  • loadTestsFromModule(‘测试用例文件名/模块名’, , pattern=’test_*.py’) - 通过测试模块加载其中的所有测试用例,生成测试套件

  • loadTestsFromName(‘测试用例文件名/测试用例类名通用’) - 通过字符串格式的测试函数导入路径名

  • loadTestsFromNames(names, module=None):通过测试函数导入路径名,批量加载测试用例

  • getTestCaseNames(testCaseClass):通过测试类获取其中所有测试函数的测试函数导入路径名

  • discover(start_dir=’./‘, pattern=’test_*.py’) 模块过滤装载器

    ​ start_dir筛选路径, pattern筛选文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import unittest

# 创建测试套件
suite = unittest.TestSuite()
# 创建用例加载器 - 往测试套件添加测试用例用的
loader = unittest.TestLoader()
# 加载用例
# 通过测试类加载整个类的用例 (以test开头的方法)
from demo1.test01Demo import TestDemo
testClass01 = loader.loadTestsFromTestCase(TestDemo)
# 通过模块的方式加载测试用例, 模块就是一个py文件
from demo1 import test01Demo
testClass02 = loader.loadTestsFromModule(TestDemo)
# 通过文件夹路径,添加整个文件夹下的py模块 (pattern匹配规则以test_开头的文件)
testClass03 = loader.discover(r"C:\test\testCase\", pattern="test_*.py")

suite.addTest(TestClass01())
suite.addTest(TestClass02())
suite.addTest(TestClass03())
# or
suite.addTests([
TestClass01(),
TestClass02(),
TestClass03()
])

获取套件中的用例数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import unittest

# 创建测试套件
suite = unittest.TestSuite()
# 创建用例加载器 - 往测试套件添加测试用例用的
loader = unittest.TestLoader()
'''
其他: suite 属性
_testMethodName:字符串类型,测试方法(函数)名
_testMethodDoc:字符串类型,测试方法(函数)docstring注释
_outcome: 用例结果,unittest._Outcome对象,包含测试结果result(TestResult对象)及用例过执行异常errors等
id():用例标识,为用例的导入路径,例如test_demo.TestDemo.test_a
shortDescription():用例描述,_testMethodDoc的第一行内容
defaultTestResult():默认使用的测试结果result对象(新建一个TestResult对象)
countTestCases():用例数量,固定为1
run(result=None):用例运行方法
debug():用例调试方法,不纪录测试结果
fail(msg=None):使用例失败
skipTest(reason):使用例跳过(抛出SkipTest异常)

for test in suite:
print('用例id:', test.id())
print('用例描述:', test.shortDescription())
print('测试方法(函数)名:', test._testMethodName)
print('测试方法(函数)完整docstring:', test._testMethodDoc)
print('所属测试类(名称):', test.__class__.__name__)

# 可以通过 用例 测试类对象 + 对应的测试方法名 获取测试方法对象
testMethod = getattr(test.__class__, test._testMethodName)
print('测试方法(函数)对象:', testMethod)
# 通过测试方法对象可以拿到很多相关信息
print('测试方法(函数)名:', testMethod.__name__) # 同test._testMethodName
print('测试方法(函数)完整docstring:', testMethod.__doc__) # 同test._testMethodDoc

'''

# 用例数
caseNumber = suite.countTestCases()
print(caseNumber)

执行测试套件

unittest的TestResult 类方法

常用方法

  • wasSuccessful():是否全部成功
  • stop():停止当前测试运行
  • startTest(test):开始(纪录)测试某用例
  • stopTest(test):停止(纪录)测试某用例
  • startTestRun():开始(纪录)整体的测试运行
  • stopTestRun():停止(纪录)整体的测试运行
  • addError(test, err):纪录异常用例
  • addFailure(test, err):纪录失败的用例
  • addSuccess(test):纪录成功的用例(默认什么都不做)
  • addSkip(test, reason):纪录跳过的测试用例
  • addExpectedFailure(test, err):纪录期望失败的测试用例
  • addUnexpectedSuccess(test):纪录非预期成功的测试用例
  • addSubTest(test, subtest, outcome):纪录子测试

unittest - 用例加载器&执行测试套件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import unittest

# 创建测试套件
suite = unittest.TestSuite()
# 创建用例加载器 - 往测试套件添加测试用例用的
loader = unittest.TestLoader()
'''
... 添加用例逻辑
'''
# 运行测试套件所有的用例
'''
TextTestRunner() __init__ 方法参数:
stream = f f为文件流,测试结果将会写入到该文件流中
verbosity = 0 默认值为0,默认输出; 2 详细输出(测试类,测试用例,测试结果)
'''
runner = unittest.TextTestRunner()
runner.run(suite)

直接传入路径加载放入到测试套件中

1
2
3
4
5
6
7
8
9
10
import unittest

# 创建测试套件
suite = unittest.TestSuite()

# 通过路径,自动发现测试用例
suite = unittest.defaultTestLoader.discover(r'C:\test\testCase\')

runner = unittest.TextTestRunner()
runner.run(suite)

使用第三方库 BeautfulReport 生成html报告

1
2
3
4
5
6
7
8
9
10
import unittest
import BeautfulReport as HtmlReport
# 创建测试套件
suite = unittest.TestSuite()

# 通过路径直接加载全部用例
suite = unittest.defaultTestLoader.discover(r'C:\test\testCase\')

runner = HtmlReport(suites=suite)
runner.report(description="测试报告描述")

进阶

装饰器

  • @unittest.expectedFailure

    ​ 测试用例结果为失败时,则pass

  • @unittest.skip(‘跳过的原因’)

    ​ 无条件跳过装饰测试。

  • @unittest.skipIf(‘跳过条件’, ‘跳过的原因’)

    ​ 如果条件为 true,则跳过修饰测试

  • @unittest.skipUnless(‘不跳过的条件’, ‘不跳过的原因’)

    ​ 条件为 false,则跳过修饰测试

测试覆盖率

coverage.py是一个用于评估Python代码测试覆盖率的工具。它可以帮助你确定你的测试用例是否足够全面地覆盖了代码,并且可以生成报告来帮助你识别哪些代码没有被测试到。

使用coverage.py很简单,只需要在运行测试之前安装它,然后在测试运行时使用coverage run命令来运行测试。

  • 安装coverage

    1
    pip install coverage
  • 执行

    1
    coverage run test_divi.py
  • 生成文件

  • htmlcov 数据生成html

    1
    coverage html

数据驱动测试DDT

数据驱动测试的基本概念、引读

  • 当我们进行测试时遇到执行步骤相同,只需要改变入口参数的测试时,使用DDT可以简化代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 示例:
# 首先,我们观察这三个测试用例,我们会发现,三个测试用例除了入口参数需要变化,
# 其测试执行语句都是相同的,因此,为了简化测试代码,我们可以使用数据驱动测试的理论将三个方法写作一个方法

# 未使用数据驱动测试的代码:
class BasicTestCase(unittest.TestCase):
def test1(self, num1):
num = num1 + 1
print('number:', num)

def test2(self, num2):
num = num2 + 1
print('number:', num)

def test3(self, num3):
num = num3 + 1
print('number:', num)

# 使用数据驱动测试的代码,执行效果与上文代码相同此处只需要了解大概框架,详细步骤下文会解释
@ddt
class BasicTestCase(unittest.TestCase):
@data('666', '777', '888')
def test(self, num):
print('数据驱动的number:', num)
  • 单一参数的数据驱动测试

    导包——设置@ddt装饰器——写入参数——形参传递——调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 单一参数的数据驱动

# 前置步骤:
# 使用语句import unittest导入测试框架
# 使用语句from ddt import ddt, data导入单一参数的数据驱动需要的包

# 示例会执行三次test,参数分别为'666','777','888'

@ddt # 设置@ddt装饰器
class BasicTestCase(unittest.TestCase):
@data('666', '777', '888') # 设置@data装饰器,并将传入参数写进括号
def test(self, num): # test入口设置形参
print('数据驱动的number:', num)
# 程序会执行三次测试,入口参数分别为666、777、888,结果见下图
  • 多参数的数据驱动测试(一个测试参数中含多个元素)

    导包——设置@ddt装饰器——设置@unpack解包——写入参数——形参传递——调用

1
2
3
4
5
6
7
8
9
10
11
# 多参数的数据驱动
# 在单一参数包的基础上,额外导入一个unpack的包,from ddt import ddt, data, unpack
# 步骤:导包——设置@ddt装饰器——设置@unpack解包——写入参数——形参传递——调用

@ddt
class BasicTestCase(unittest.TestCase):
@data(['张三', '18'], ['李四', '19']) # 设置@data装饰器,并将同一组参数写进中括号[]
@unpack # 设置@unpack装饰器顺序解包,缺少解包则相当于name = ['张三', '18']
def test(self, name, age):
print('姓名:', name, '年龄:', age)
# 程序会执行两次测试,入口参数分别为['张三', '18'],['李四', '19'],测试结果见下图

txt格式文件驱动

  • 单一参数数据驱动

    编写阅读数据文件的函数、@data入口参数加*读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 单一参数txt文件
# 新建num文件,txt格式,按行存储777,888,999
# num文件内容(参数列表):
# 777
# 888
# 999
# 编辑阅读数据文件的函数
# 记住读取文件一定要设置编码方式,否则读取的汉字可能出现乱码!!!!!!
def read_num():
lis = [] # 以列表形式存储数据,以便传入@data区域
with open('num', 'r', encoding='utf-8') as file: # 以只读'r',编码方式为'utf-8'的方式,打开文件'num',并命名为file
for line in file.readlines(): # 循环按行读取文件的每一行
lis.append(line.strip('\n')) # 每读完一行将此行数据加入列表元素,记得元素要删除'/n'换行符!!!
return lis # 将列表返回,作为@data接收的内容
@ddt
class BasicTestCase(unittest.TestCase):
@data(*read_num()) # 入口参数设定为read_num(),因为返回值是列表,所以加*表示逐个读取列表元素
def test(self, num):
print('数据驱动的number:', num)
  • 多参数数据驱动

    读取函数中的数据分割、@unpack解包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 多参数txt文件
# dict文件内容(参数列表)(按行存储):
# 张三,18
# 李四,19
# 王五,20
def read_dict():
lis = [] # 以列表形式存储数据,以便传入@data区域
with open('dict', 'r', encoding='utf-8') as file: # 以只读'r',编码方式为'utf-8'的方式,打开文件'num',并命名为file
for line in file.readlines(): # 循环按行读取文件的每一行
lis.append(line.strip('\n').split(',')) # 删除换行符后,列表为['张三,18', '李四,19', '王五,20']
# 根据,分割后,列表为[['张三', '18'], ['李四', '19'], ['王五', '20']]
return lis # 将列表返回,作为@data接收的内容
@ddt
class BasicTestCase(unittest.TestCase):
@data(*read_dict()) # 加*表示逐个读取列表元素,Python中可变参数,*表示逐个读取列表元素,列表为[['张三', '18'], ['李四', '19'], ['王五', '20']]
@unpack # 通过unpack解包,逐个传参,缺少这句会将['张三', '18']传给name,从而导致age为空
def test(self, name, age): # 设置两个接收参数的形参
print('姓名为:', name, '年龄为:', age)

json格式文件驱动

  • 单一参数数据驱动

    使用json解析包读取文件

1
2
3
4
5
6
7
8
9
10
11
12
# 单一参数——json文件
# num.json文件内容(参数列表)(注意命名后缀):
# ["666","777","888"]
# 注意JSON文件中,数据元素如果是字符串必须得用双引号
# 使用语句import json导入json包,快速读取文件用
def read_num_json():
return json.load(open('num.json', 'r', encoding='utf-8')) # 使用json包读取json文件,并作为返回值返回,注意读取的文件名
@ddt # 数据驱动步骤和txt相同
class BasicTestCase(unittest.TestCase):
@data(*read_num_json())
def test(self, num):
print('读取的数字是', num)
  • 多参数数据驱动(以列表形式存储多参数)

    @unpack装饰器的添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 数据分离
# 多参数——json文件
# 步骤和单一参数类似,仅需加入@unpack装饰器以及多参数传参入口
# dict文件内容(参数列表)(非规范json文件格式):
# [["张三", "18"], ["李四", "19"], ["王五", "20"]]
# 注意json文件格式中字符串用双引号
def read_dict_json():
return json.load(open('dict.json', 'r', encoding='utf-8')) # 使用json包读取json文件,并作为返回值返回
@ddt
class BasicTestCase(unittest.TestCase):
@data(*read_dict_json())
@unpack # 使用@unpack装饰器解包
def test(self, name, age): # 因为是非规范json格式,所以形参名无限制,下文会解释规范json格式
print('姓名:', name, '年龄:', age)
  • 多参数数据驱动(以对象形式存储多参数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 规范json格式读取,每一组参数以对象形式存储
# dict文件内容:
# [
# {"name":"张三", "age":"18"},
# {"name":"李四", "age":"19"},
# {"name":"王五", "age":"20"}
# ]

def read_dict_json():
return json.load(open('dictx.json', 'r', encoding='utf-8')) # 使用json包读取json文件,并作为返回值返回
@ddt
class BasicTestCase(unittest.TestCase):
@data(*read_dict_json())
@unpack
def test(self, name, age): # 令形参名字和json中命名相同name=name,age=age
print('姓名:', name, '年龄:', age)

# 非常特殊情况:
# 形参名字和json中对象命名无法相同,则更改读取函数
# 提取已读完后的json文件(字典形式),通过遍历获取元素,并返回
# def read_dict_json():
# li = []
# dic = json.load(open('dict.json', 'r', encoding='utf-8'))
# # 此处加上遍历获取语句,下文yaml格式有实例,方法一样
# return li

yaml格式文件驱动

​ 在unittest测试框架中,对yaml数据格式的支持十分强大,使用非常方便

​ yaml文件的数据驱动执行代码十分简单!!!(但是要注意细节)

  • 单一参数数据驱动

    使用yaml解析包读取文件,导入file_fata驱动数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# YAML数据格式驱动
# 单一参数
# import yaml # 导入yaml解析包
# from ddt import file_data # 导入file_data驱动数据
# yaml格式文件内容
# - 666
# - 777
# - 888
# '-'号之后一定要打空格!!!
@ddt
class BasicTestCase(unittest.TestCase):
@file_data('num.yml') # 采用文件数据驱动
def test(self, num):
print('读取的数字是', num)
  • 多参数数据驱动

    ​ 形参入口和数据参数key命名统一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 多参数yaml
# 以对象形式存储yml数据(字典)
# yaml格式文件内容
# -
# name: 张三
# age: 18
# -
# name: 李四
# age: 19
# -
# name: 王五
# age: 20
# '-'号之后一定要打空格!!!
# ':'号之后一定要打空格!!!

# 入口参数与数据参数key命名统一即可导入
@ddt
class BasicTestCase(unittest.TestCase):
@file_data('dict.yml')
def test(self, name, age): # 设置入口参数名字与数据参数命名相同即可
print('姓名是:', name, '年龄为:', age)
  • 特殊情况:当入口与文件中数据参数无法统一命名时,解决办法
1
2
3
4
5
6
# 入口参数与数据参数命名不统一
@ddt
class BasicTestCase(unittest.TestCase):
@file_data('dict.yml')
def test(self, **cdata): # Python中可变参数传递的知识:**按对象顺序执行
print('姓名是:', cdata['name'], '年龄为:', cdata['age']) # 通过对象访问语法即可调用

案例

开发一个接口测试框架

需求:

  • 支持用例优先级、标签、支持通过优先级或标签帅选用例
  • 支持用例负责人、迭代,以及通过负责人或迭代人筛选用例
  • 支持多环境配置
  • 支持超时重试机制,防止不稳定用例
  • 并发执行用例以提高用例回归效率

为用例添加额外属性

  • 支持用例优先级、标签、支持通过优先级或标签帅选用例
  • 支持用例负责人、迭代,以及通过负责人或迭代人筛选用例

实现的步骤为:

  1. 编写用例时,在测试函数上添加特殊标记
  2. 正常加载用例生成测试套件
  3. 遍历测试套件所有测试用例,根据条件(如优先级为P0),和用例特殊标记筛选生成新的测试套件
  • 方法一:测试函数注释,如在测试函数注释第一行以外添加特定格式字符串如priority:0等
  • 方法二:测试函数对象,为测试函数对象添加额外属性,可以通过装饰器实现
  • 方法三:测试函数名,如test_a_p0,多种标签可能导致用例名太长或标识度不高 (不推荐)

通过测试函数注释-添加用例优先级属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 测试用例
import re
import unittest

def get_suite_tests(suite):
tests = []
for test in suite:
if isinstance(test, unittest.TestCase):
tests.append(test)
else:
tests.extend(get_suite_tests(test))
return tests

class TestDemo(unittest.TestCase):
def test_a(self):
"""测试a
priority:0
"""
def test_b(self):
"""测试b
priority:1
"""
def test_c(self):
"""测试c
priority:1
"""

# 测试套件
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestDemo)
new_suite = unittest.TestSuite() # 新建测试套件(用于放筛选的用例)

# 筛选p0用例
for test in get_suite_tests(suite):
# 通过正则匹配获取用例优先级
matched = re.search(r'priority:(\d)', test._testMethodDoc)
if matched:
priority = matched.group(1)
else:
priority = None
print(f"用例:{test._testMethodName} 优先级: {priority}")
# 筛选p0用例-组成新测试套件
if priority == 0:
new_suite.addTest(test)

print('筛选得到的用例数量:', new_suite.countTestCases())

通过测试函数对象-添加额外属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import unittest

def get_suite_tests(suite):
tests = []
for test in suite:
if isinstance(test, unittest.TestCase):
tests.append(test)
else:
tests.extend(get_suite_tests(test))
return tests

def test(priority=None, tags=None, owner=None, iteration=None):
"""装饰器,为测试函数添加额外属性"""
def decorator(func):
# 根据参数为测试函数添加额外属性
func.priority = priority
func.tags = tags
func.owner = owner
func.iteration = iteration
return func
return decorator

# 用例
class TestDemo(unittest.TestCase):
@test(priority=0, tags=['demo'], owner='superhin', iteration='v1.0.0')
def test_a(self):
"""测试a"""

@test(priority=1, tags=['demo'], owner='superhin', iteration='v1.0.0')
def test_b(self):
"""测试b"""

@test(priority=1, tags=['demo'], owner='superhin', iteration='v2.0.0')
def test_c(self):
"""测试c"""

# 测试套件
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestDemo)
new_suite = unittest.TestSuite() # 新建测试套件(用于放筛选的用例)

# 筛选p0用例
for test in get_suite_tests(suite):
testMethod = getattr(test.__class__, test._testMethodName)
if hasattr(testMethod, 'priority') and getattr(testMethod, 'priority') == 0:
new_suite.addTest(test)
print('筛选得到的用例数量:', new_suite.countTestCases())

通过测试类-类属性设置用例通用属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import unittest

def get_suite_tests(suite):
tests = []
for test in suite:
if isinstance(test, unittest.TestCase):
tests.append(test)
else:
tests.extend(get_suite_tests(test))
return tests

def safe_getattr(obj, attr):
"""对象没有该属性时返回None"""
if hasattr(obj, attr):
return getattr(obj, attr)

def test(priority=None, tags=None, owner=None, iteration=None):
"""装饰器,为测试函数添加额外属性"""
def decorator(func):
# 根据参数为测试函数添加额外属性
func.priority = priority
func.tags = tags
func.owner = owner
func.iteration = iteration
return func
return decorator

class TestCase(unittest.TestCase):
def __init__(self, methodName='runTest'):
super().__init__(methodName)
testMethod = getattr(self, methodName)
testClass = self.__class__
# 拷贝测试方法(函数)属性到测试用例对象,测试方法无属性时尝试获取测试类对象指定属性
self.priority = safe_getattr(testMethod, 'priority') or safe_getattr(testClass, 'priority')
self.tags = safe_getattr(testMethod, 'tags') or safe_getattr(testClass, 'tags')
self.owner = safe_getattr(testMethod, 'owner') or safe_getattr(testClass, 'owner')
self.iteration = safe_getattr(testMethod, 'iteration') or safe_getattr(testClass, 'iteration')

class TestDemo(TestCase):
owner = 'superhin'
iteration = 'v1.0.0'
tags = ['demo']

@test(priority=0)
def test_a(self):
"""测试a"""

@test(priority=1)
def test_b(self):
"""测试b"""

@test(priority=1, iteration='v2.0.0')
def test_c(self):
"""测试c"""

# 原测试套件
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestDemo)
new_suite = unittest.TestSuite() # 新建测试套件(用于放筛选的用例)

# 筛选iteration='v1.0.0'用例
for test in get_suite_tests(suite):
print("用例适用版本:", safe_getattr(test, 'iteration'))
if safe_getattr(test, 'iteration') == 'v1.0.0':
new_suite.addTest(test)
print('筛选得到的用例数量:', new_suite.countTestCases())

测试计划-通过属性筛选测试用例

​ 增加一个自定义的TestPlan对象,通过类属性描述

1
2
3
4
5
6
7
8
9
10
11
class TestPlanDemo1(TestPlan):
# 测试目录(该目录所有的测试用例)
test_dir = '../testcases'
# 可选, 支持priorities, owners, iterations, tags, exclude_tags等
filter = {
"priorities": [0, 1],
"tags": ["demo"],
}

if __name__ == '__main__':
TestPlanDemo1().run(verbosity=2)

​ 用例过滤函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def filter_by_priorities(tests, priorities):
"""通过优先级列表筛选用例,返回筛选后用例对象列表"""
new_tests = []
for test in tests:
if safe_getattr(test, 'priority') in priorities:
new_tests.append(test)
return new_tests

def filter_by_tags(tests, tags):
"""通过tags筛选用例,返回筛选后用例对象列表"""
new_tests = []
for test in tests:
test_tags = safe_getattr(test, 'tags') or []
print(tags, test_tags)
# 测试用例tags集合不完全包含指定tags集合
# 通过差集:部分(指定tags) - 整体(用例tags), 为空集时, 则整体完全包含部分
if set(tags) - set(test_tags) == set():
new_tests.append(test)
return new_tests

# ... 其他筛选方法

​ 测试计划类实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class TestPlan:
test_dir: str = None
tests = []
filter: dict = None

def __init__(self):
# 组装测试套件
loader = unittest.defaultTestLoader
if self.tests: # 通过用例名称-生成测试套件
suite = loader.loadTestsFromNames(self.tests)
elif self.test_dir: # 通过测试目录-遍历生成测试套件
print('self.test_dir', self.test_dir)
suite = loader.discover(start_dir=self.test_dir)
else:
raise ValueError("测试计划必须包含start_dir或tests属性")

# 过滤用例
if self.filter:
tests = get_suite_tests(suite)
if 'priorities' in self.filter:
tests = filter_by_priorities(tests, self.filter['priorities'])

if 'tags' in self.filter:
tests = filter_by_tags(tests, self.filter['tags'])
# ... 其他筛选条件

# 根据筛选的用例列表生成新的测试套件
suite = unittest.TestSuite()
suite.addTests(tests)

self.suite = suite

def run(self, verbosity=1):
runner = unittest.TextTestRunner(verbosity=verbosity)
runner.run(self.suite)