一、pytest运行方式与基本规范
1.简单介绍
pytest是一个非常成熟的全功能的Python测试框架,主要有以下几个特点:
- 简单灵活,容易上手
- 支持参数化
- 能够支持简单的单元测试和复杂的功能测试,还可以用来做Web UI自动化测试(pytest+selenium)、接口自动化测试(pytest+requests)
- pytest具有很多第三方插件,并且可以自定义扩展,比较好用的如pytest-ordering(用例顺序控制)、pytest-rerunfailures(失败case重复执行)、pytest-dependency(用例依赖)等
- 测试用例的skip和xfail处理
- 可以很好的和jenkins集成
- report框架----allure 也支持了pytest
2.pytest用例设计原则
- 文件名格式:test_*.py 或 *_test.py
- 类名以 Test 开头,不能包含 init 方法
- 函数名以 test_ 开头或 _test结尾
3.运行方式
####(1)主函数运行
- 运行所有:pytest.main()
- 指定参数:pytest.main([‘-vs’])
- 指定模块:pytest.main([‘-vs’,‘xxxx.py’])
- 指定目录:pytest.main([‘-vs’,‘./文件名称’])
- pytest支持单个函数写法:pytest.main([‘-vs’,‘./文件名/py文件名::类名::方法名’])
####(2)命令行运行 - 运行所有:pytest
- 指定参数:pytest -vs
- 指定模块:pytest xxxx.py
- 指定目录:pytest testcase
- nodeid 指定运行用例:pytest dir_name::xxx.py::TestClass::test_func
####(3)常用参数 - -q:只打印测试用例的执行结果,不输出环境信息
- -s:打印详细日志,可显示程序中的 print/logging 输出
- -v:打印详细的用例执行日志信息
- -x:遇到测试失败的用例则立刻停止运行
- -k expr:按关键字表达式运行用例,如 expr 为 “test_a or test_b” 则运行 test_a 和 test_b
- -m slow:运行使用 mark 标记的测试用例,如:-m slow 则执行所有带 @pytest.mark.slow 装饰器的用例
- –resultlog=./log.txt 生成log
- -r:测试结束时显示结果摘要,后面可以跟多个字符,其中 f:失败的用例、E:出错的用例、s:跳过的用例、x:标记失败的用、X:标记成功的用例、p:成功的用例、P:成功用例并输出信息、a:除开 pP 以外的用例、A 全部用例
- -l:展示运行过程中的全局变量和局部变量
- –maxfail=n:在第n个失败后停止
- –reruns 失败重跑
- –lf, --last-failed:只重新运行失败。
- –ff, --failed-first:先运行故障,然后运行其余测试。
二、用例前置、后置处理(fixture)
fixture属于pytest中的一个方法。fixture可以用作测试用例的前置和后置操作,其中fixture命令规范没有像setup和teardown固定格式。可以随意命名。控制fixture的前置和后置操作是通过yield关键字进行来区分的,代码在yield前面的属于前置操作,代码在yield后面的属于后置操作。并且fixture也没有强烈的要求必须要前后置同时存在,可以只存在前置也可以只存在后置。fixture如果有后置内容,无论遇到什么问题,都会进行执行后置的代码。
注意:在conftest.py文件中的多个fixture函数还可以互相调用,但是要注意的是,比如fixture1调用fixture2时,那么fixture1的scope范围一定要比fixture2的范围小,否则就会报错
1、基本使用
# 定义一个夹具函数,使用装饰器pytest.fixture
@pytest.fixture()
def login():
print("用户登录")
yield
print("用户登出")
# 方式1:作为参数传递
def test_demo_01(login):
print("执行 test_demo_01 用例")
# 方式2:使用 pytest.mark.usefixtures 装饰器使用指定fixture,可以传入多个 fixture,执行顺序依次执行
@pytest.mark.usefixtures("login")
def test_demo_02():
print("执行 test_demo_02 用例")
2、fixture函数参数
语法:@pytest.fixture(scope=“function”, params=None, autouse=False, ids=None, name=None)
参数解释:
- scope:控制fixture的作用范围。类似于setup_module、setup_class等。取值如下:
取值 | 作用 |
---|---|
function | 函数级 每一个函数或方法都会调用 |
class | 类级 每一个类都会被调用 |
module | 模块级 每一个.py文件调用一次 |
session | 会话级 次会话只需要运行一次,会话内所有方法及类,模块都共享这个方法 |
- autouse:默认是 False ,若为 True,则每个测试函数都会自动调用该 fixture ,无需传入 fixture 函数名。
@pytest.fixture(autouse=True)
def fix_demo():
print("我是 fix_demo 的返回值")
# 不需要传入fixture函数
def test_demo_03():
print("执行 test_demo_03 用例")
执行结果:
test_demo_file_01.py::test_demo_03 我是 fix_demo 的返回值
执行 test_demo_03 用例
PASSED
- name:fixture 的重命名。重命名后原函数名将不生效
@pytest.fixture(name="new_fix_demo")
def fix_demo():
print("我是 fix_demo 的返回值")
# 传入fixture函数名
def test_demo_03(fix_demo):
print("执行 test_demo_03 用例")
# 传入重命名的fixture名称
def test_demo_04(new_fix_demo):
print("执行 test_demo_04 用例")
#执行后 test_demo_03 用例将报错:fixture 'fix_demo' not found
- params:fixture 的可选形参列表,支持列表传入,默认None,每个param的值,fixture都会去调用执行一次,类似for循环,可与参数ids一起使用,作为每个参数的标识,用法见ids。被fixture装饰的函数要调用采用:request.param(固定写法)
@pytest.fixture(params=["张三", "李四", "王五"])
def fix_demo(request):
temp = request.param
print("我是{0},我来签到".format(temp))
return temp
def test_user(fix_demo):
print("{0}已签到".format(fix_demo))
运行结果:
test_demo_file_01.py::test_user[\u5f20\u4e09] 我是张三,我来签到
张三已签到
PASSED
test_demo_file_01.py::test_user[\u674e\u56db] 我是李四,我来签到
李四已签到
PASSED
test_demo_file_01.py::test_user[\u738b\u4e94] 我是王五,我来签到
王五已签到
PASSED
# 或者与pytest.mark.parametrize装饰器一起使用
@pytest.fixture()
def fix_demo(request):
temp = request.param
print("我是{0},我来签到".format(temp))
return temp
@pytest.mark.parametrize("fix_demo", ["张三", "李四", "王五"], indirect=True)
def test_user(fix_demo):
print("{0}已签到".format(fix_demo))
- ids:用例标识ID,与params配合使用,一对一关系
@pytest.fixture(params=["张三", "李四", "王五"], ids=["case01", "case02", "case03"])
def fix_demo(request):
temp = request.param
print("我是{0},我来签到".format(temp))
return temp
def test_user(fix_demo):
print("{0}已签到".format(fix_demo))
运行结果:
test_demo_file_01.py::test_user[case01] 我是张三,我来签到
张三已签到
PASSED
test_demo_file_01.py::test_user[case02] 我是李四,我来签到
李四已签到
PASSED
test_demo_file_01.py::test_user[case03] 我是王五,我来签到
王五已签到
PASSED
3、fixture 对比 setup、teardown
- conftest.py专为fixture而生,全局共享conftest.py,可以在不同目录下创建conftest.py。可以统一管理、多处调用和自动调用
- 命名方式灵活,不局限于setup和teardown命名方式
- 自动引入不需要导入
- fixture出现重名,就近原则。查找路径:当前模块->当前目录->上一层目录->…->项目root目录(不会去平级的目录查找)
4、yield 和 addfinalizer 终结函数
yield在python中表示生成器,同时也是一种函数的返回值类型,是函数上下文管理器,使用yield被调fixture函数执行遇到yield会停止执行,接着执行调用的函数,调用的函数执行完后会继续执行fixture函数yield关键后面的代码。
@pytest.fixture()
def login_1():
print("开始登录")
result = "登录后的token"
yield result
print("退出登录")
def test_demo_01(login_1):
a = login_1
print(a)
print("执行 test_demo_01 用例")
运行结果:
test_demo_file_01.py::test_demo_01 开始登录
登录后的token
PASSED退出登录
三、常见的处理方法
1、用例失败重试
用例在运行过程可能因为环境原因、网络波动、莫名异常或网络延迟造成用例获取响应失败等。通过引入失败用例重跑机制来消除因为网络不稳定而引起的用例运行失败。
插件官网说明:https://pypi.org/project/pytest-rerunfailures/
安装pytest插件:pip install pytest-rerunfailures 。使用方式有以下两种:
(1)命令行参数形式
pytest --reruns 重试次数
pytest --reruns 重试次数 --reruns-delay 次数之间的延时间隔设置(单位:秒)
# 文件 test_demo_file_01.py
def test_demo_01(login):
r = random.randint(1, 2)
assert r == 1
def test_demo_02(login):
b = 1 + 2
assert b == 3
执行:pytest --reruns 3 --reruns-delay 3 test_demo_file_01.py
(2)装饰器方式
语法:@pytest.mark.flaky(reruns=重试次数, reruns_delay=次数之间的延时设置(单位:秒)) # reruns_delay 非必须
# 文件 test_demo_file_01.py
@pytest.mark.flaky(reruns=3, reruns_delay=3)
def test_demo_01():
r = random.randint(1, 2)
assert r == 1
2、用例跳过
在实际测试场景中,会遇到功能阻塞、功能未实现或环境等外部因素导致用例无法正常执行的情况,此时可以使用pytest提供的 skip 和 skipif 来跳过用例。
(1)无条件跳过
语法:
@pytest.mark.skip() # 跳过类、方法或用例,不备注原因
@pytest.mark.skip(reason=“”) # 跳过类、方法或用例,备注原因
# 文件 test_demo_file_01.py
@pytest.mark.skip(reason="跳过这个用例")
def test_demo_02():
b = 1 + 2
assert b == 3
执行结果:
test_demo_file_01.py::test_demo_02 SKIPPED (跳过这个用例)
(2)有条件跳过
语法:@pytest.mark.skipif(contdition, reason=“”) # 满足条件时跳过类、方法或用例,需备注原因
# 文件 test_demo_file_01.py
@pytest.mark.skipif(1 == 1, reason="跳过整个测试类")
class TestDemo:
def test_func_01(self):
print("test_func_01")
def test_func_02(self, login):
print("test_func_02")
运行结果:
test_demo_file_01.py::TestDemo::test_func_01 SKIPPED (跳过整个测试类)
test_demo_file_01.py::TestDemo::test_func_02 SKIPPED (跳过整个测试类)
(3)用例内跳过
语法:@pytest.skip(reason=“”) # 在用例内部执行判断,不满足条件时跳过用例
# 文件 test_demo_file_01.py
class TestDemo:
def test_func_01(self):
a = random.randint(1, 2)
if a == 1:
pytest.skip(reason="不满足执行条件,跳过该用例")
print("test_func_01")
def test_func_02(self):
print("test_func_02")
运行结果:
test_demo_file_01.py::TestDemo::test_func_01 SKIPPED (不满足执行条件,跳过该用例)
test_demo_file_01.py::TestDemo::test_func_02 test_func_02 PASSED
(4)模块级跳过
语法:pytest.skip(reason=“”, allow_moudle_level=false) # 跳过整个模块,参数必填allow_module_level=True
import pytest
pytest.skip(reason="跳过整个模块", allow_module_level=True)
class TestDemo:
def test_func_01(self):
# a = random.randint(1, 2)
# if a == 1:
# pytest.skip(reason="不满足执行条件,跳过该用例")
print("test_func_01")
def test_func_02(self):
print("test_func_02")
运行结果:
Skipped: 跳过整个模块
collected 0 items / 1 skipped
(5)缺少某些导入时跳过用例
语法:@pytest.importorskip( modname: str, minversion: optional[str] = none, reason: optional[str] = none )
参数解释:
- modname: 需要被导入的模块名称,比如 selenium;
- minversion:表示需要导入的最小的版本号,如果该版本不达标,将会打印出报错信息;
- reason:只有当模块没有被导入时,给定该参数将会显示出给定的消息内容
# 测试模块
import pytest
rock = pytest.importorskip("rock")
@rock
def test_1():
print("测试是否导入了rock模块")
# 测试模块版本
import pytest
sel = pytest.importorskip("selenium", minversion="3.150")
@sel
def test_1():
print("测试是否导入了selenium模块")
(6)skip赋值变量,多处调用
# 首先定义一个变量
myskip = pytest.mark.skip(reason="未知原因")
myskipif = pytest.mark.skipif(1==1, reason="未知原因")
# 然后在其它模块用例导入这个变量,使用时直接 @myskip @myskipif
@myskip
def test_func_02(self):
print("test_func_02")
3、用例标记或分组
在日常测试过程中,当有很多测试用例,但只想执行其中的一部分用例,可以使用@pytest.mark.自定义标签功能满足。
使用mark灵活给测试用例打标签或分组。在运行测试用例的时候,可以根据标签名来过滤运行的用例。
(1)标签规范建议
# 1.按环境
@pytest.mark.development
@pytest.mark.release
...
# 2.按作者
@pytest.mark.zhangsan
@pytest.mark.lisi
...
# 3.按版本
@pytest.mark.v1
@pytest.mark.v2
...
# 4.按用途(回归、冒烟、全量等)
@pytest.mark.smoke
@pytest.mark.full
...
(2)标签使用
测试用例和测试类都支持单标签和多标签。给测试类打标签时,生效的对象是这个测试类下所有的用例。
# 对测试类打标签时可以使用以下两种方式
# 方式一:
@pytest.mark.zhangsan
@pytest.mark.smoke
class TestDemo:
def test_func_01(self):
print(" excute test_func_01")
def test_func_02(self):
print("test_func_02")
# 方式二:
class TestDemo:
# 单标签
# pytestmark = pytest.mark.full
# 多标签
pytestmark = [pytest.mark.zhangsan, pytest.mark.smoke]
def test_func_01(self):
print(" excute test_func_01")
def test_func_02(self):
print("test_func_02")
(3)执行/不执行指定标签的用例
- 执行指定标签的用例
pytest test_demo_file_01.py -m smoke - 不执行指定标签的用例
pytest test_demo_file_01.py -m “not smoke” - 多标签执行
pytest test_demo_file_01.py -m “zhangsan or smoke”
(4)标签管理
自定义标签在执行的时候会提示告警信息,如下:
PytestUnknownMarkWarning: Unknown pytest.mark.smoke - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html
解决方法一:
在项目的 pytest.ini 文件中添加标签集合,如下:
[pytest]
markers =
smoke: explain for this
success
.....
解决方法二:
在 conftest.py 文件中注册,多个标签添加多条记录,如下:注册 regression、smoke、zhangsan 三个自定义标签
# 固定插件用法
def pytest_configure(config):
config.addinivalue_line("markers", "regression: only regression test")
config.addinivalue_line("markers", "smoke: only smoke test")
config.addinivalue_line("markers", "zhangsan: only author test")
4、用例执行顺序和依赖
(1)用例执行顺序
pytest默认执行用例顺序是根据项目下文件名称按ascii码去收集运行的,文件里的用例是从上往下按顺序执行的(模块级会先以模块名按ascii编码进行排序)。
当需要调整测试用例执行顺序时,可以引入插件 pytest-ordering 来控制用例执行顺序。
安装:pip install pytest-ordering
语法:@pytest.mark.run(order=n) # 按照n值顺序执行,order为非负整数(值越小优先级越高) > 无排序装饰器 > order为负整数(负的值越大优先级越高)
@pytest.mark.run(order=3)
def test_01():
print("test_01")
time.sleep(1.0)
@pytest.mark.run(order=2)
def test_two():
print("test_two")
time.sleep(10)
@pytest.mark.run(order=1)
def test_regin():
print("用例test_regin")
time.sleep(1.5)
def test_login():
print("用例login")
time.sleep(0.1)
def test_05():
print("用例5")
time.sleep(2.3)
运行结果:
test_demo_file_02.py::test_regin 用例test_regin
PASSED
test_demo_file_02.py::test_two test_two
PASSED
test_demo_file_02.py::test_01 test_01
PASSED
test_demo_file_02.py::test_login 用例login
PASSED
test_demo_file_02.py::test_05 用例5
PASSED
(2)用例依赖
安装:pip install pytest-dependency
语法:@pytest.mark.dependency(name=None, depends=[], scope=‘module’)
参数解释:
name ( str) – 用于依赖测试引用的测试名称。如果未设置,则默认为 pytest 定义的节点 ID。名称必须是唯一的。
depends (iterable of str) – 依赖项,该测试所依赖的测试名称列表。除非所有依赖项都已成功运行,否则将跳过测试。依赖项也必须已被标记修饰。依赖项的名称必须适应范围。
scope ( str) – 搜索依赖项的范围。必须是’session’、‘package’、‘module’或’class’。默认是module
创建2个文件 test_depend_01.py、test_depend_02.py,如下:
import pytest
@pytest.mark.dependency()
def test_a():
pass
@pytest.mark.dependency()
@pytest.mark.xfail(reason="deliberate fail")
def test_b():
assert False
@pytest.mark.dependency(depends=["test_a"])
def test_c():
pass
@pytest.mark.dependency(depends=['test_b'])
def test_d():
pass
class TestClass(object):
@pytest.mark.dependency()
def test_b(self):
pass
运行结果:
test_depend_01.py::test_a PASSED
test_depend_01.py::test_b XFAIL (deliberate fail)
test_depend_01.py::test_c PASSED
test_depend_01.py::test_d SKIPPED (test_d depends on test_b) # test_d依赖test_b,test_b执行失败,所以test_d跳过执行
test_depend_01.py::TestClass::test_b PASSED
5、参数化数据驱动
参数化,就是把测试过程中的数据提取出来,通过参数传递不同的数据来驱动用例运行。pytest框架使用装饰器 @pytest.mark.parametrize 来对测试用例进行传参。
语法:@pytest.mark.parametrize(argnames, argvalues, indirect=False, ids=None, scope=None)
参数解释:
argnames:参数名称,字符串格式,多个参数用逗号隔开,如:“arg1,arg2,arg3”
argvalues:参数值,需要与argnames对应,元组或列表,如:[ val1,val2,val3 ],如果有多个参数例,元组列表一个元组对应一组参数的值,如:@pytest.mark.parametrize(“name,pwd”, [(“yy1”, “123”), (“yy2”, “123”), (“yy3”, “123”)])
indirect:默认为False,代表传入的是参数。如果设置成True,则把传进来的参数当函数执行,而不是一个参数
ids:自定义测试id,字符串列表,ids的长度需要与测试数据列表的长度一致,标识每一个测试用例,自定义测试数据结果的显示,为了增加可读性
import pytest
# fixture函数
@pytest.fixture()
def fix_demo(request):
temp = request.param
print("我是{0},我来签到".format(temp))
return temp
# 被测函数
def add(a, b):
return a + b
# 单个参数参数化
@pytest.mark.parametrize("a", [1, 2, 3, 4, 5])
def test_add_01(a): # 参数名称和个数需和parametrize传入的一致
print("a的值:", a)
assert add(a, 1) == a + 1
# 多个参数
@pytest.mark.parametrize("a, b, c", [(1, 2, 3), (4, 5, 9), ('1', '2', '12')])
def test_add_02(a, b, c): # 参数名称和个数需和parametrize传入的一致
print(f"a, b, c的值是:{a}, {b}, {c}")
assert add(a, b) == c
# 参数组合,场景总数为各参数个数相乘后结果。适用于期望结果是固定的
@pytest.mark.parametrize('x', [0, 1])
@pytest.mark.parametrize('y', [2, 3])
def test_combination(x, y):
print(f"测试组合:x={x}, y={y}")
# indirect=True, parametrize与request结合使用给fixture传参
@pytest.mark.parametrize("fix_demo", ["张三", "李四", "王五"], indirect=True)
def test_user(fix_demo):
print("{0}已签到".format(fix_demo))
# 和 pytest.param 联合使用,处理部分用例
@pytest.mark.parametrize("a, b, c",
[
(1, 2, 3),
pytest.param(2, 3, 6, marks=pytest.mark.xfail),
pytest.param('a', 'b', 'ab', marks=pytest.mark.skip),
pytest.param(9, 1, 10, id="9+1=10: pass")
])
def test_param(a, b, c):
print(f"a, b, c的值是:{a}, {b}, {c}")
assert add(a, b) == c
四、allure-pytest的一些介绍
pytest框架使用allure生成测试报告,报告内容丰富美观,功能强大,使用的插件 allure-pytest
安装:pip install allure-pytest
功能特性:
allure使用实例:
import allure
from common.logger import logger
import requests
@allure.step('这是测试步骤')
def step_1():
print("初始化数据")
@allure.epic('测试天气API接口'.center(30, '*'))
@allure.feature('测试模块')
@allure.suite('这是套件')
class TestHttpbin:
"""测试模块httpbin"""
@allure.severity('normal')
@allure.story('故事1:获取天气数据')
@allure.title('获取单个城市的天气')
@allure.description('获取深圳的天气')
@allure.testcase('测试用例地址:www.***.com')
@allure.issue('缺陷管理地址:https://www.zentao.net/')
@allure.tag('这是tag')
def test_001(self, login):
"""
测试httpbin接口:get方法
"""
step_1()
# api:host
url = 'https://tianqiapi.com/api'
params = {'version': 'v6', 'appid': 12253812, 'appsecret': 'KzT8yOpX'}
response = requests.get(url=url, params=params).json()
print('接口返回数据: %s' % response)
logger.info('接口返回数据: %s' % response)
五、项目流程简单介绍
https://gitlab.dev.21vianet.com/fan.jun3/api-request.git
基本数据流程:读取配置信息–>读取yml测试数据–>生成测试用例–>执行测试用例–>断言–>生成allure报告(–>发送邮件,钉钉推送?)
更多pytest用法参考官方文档文档:https://docs.pytest.org/en/stable/index.html