pytest 基本使用

pytest 介绍

pytest 是 python 语言下的一种单元测试框架,与 python 自带的 unittest 单元测试框架类似,相比于 unittest 框架使用起来更简洁,效率更高(java语言下的单元测试框架是junit和testng)

pytest 可以结合 requests 实现接口测试,结合 selenium/playwright、appium 实现自动化功能测试。

pytest 特点:

  1. 具有unittest绝大部分功能,非常容易上手,入门简单,功能灵活,文档丰富,文档中有很多实例可以参考
  2. 具有强大灵活的fixture固件,支持简单的单元测试和复杂的功能测试;
  3. 支持参数化(parametrize)、数据驱动
  4. 执行测试过程中可以将某些测试跳过(skip),或者对某些预期失败的case标记成失败(xfail)
  5. 支持重复执行(rerun)失败的case
  6. 支持运行由nose ,unittest编写的测试case
  7. 支持执行部分用例(比如:根据标记,或者features、stories,后者一来allure - pytest插件)
  8. 具有很多第三方插件(顺序控制pytest - ordering、allure报告allure - pytest、多线程pytest - xdist、……),并且可以自定义开发插件
  9. 可生成html报告,也可以结合allure生成精美的测试报告
  10. 方便的和持续集成工具jenkins集成

unittest 和 pytest 的区别:

不同点 unittest pytest
命名 测试方法以test开头 模块名必须以 test_ 开头,或者_test 结尾

类以Test开头

方法/函数以test开头
框架结构 写case必须定义类,测试类要继承unitttest.TestCase 不需要继承,可以是函数也可以是类中方法
测试报告 使用HTMLTestRunner 使用allure-pytest
数据驱动 参数化使用第三方ddt 参数化使用自带的parametrize
断言 断言库丰富 采用python断言:使用assert关键字
失败重试 不支持 支持
固件 - 灵活的fixture固件
扩展性 - 快速自定义插件开发

pytest 编写规则

1、测试模块:测试文件通常以 test_ 开头或结尾,这样 pytest 可以自动发现这些文件。例如:test_example.py 或者 example_test.py

1
2
3
4
5
6
# test_example.py
def test_addition():
assert 1 + 1 == 2

def test_subtraction():
assert 2 - 1 == 1

2、测试类

  • 测试类名称应以 Test 开头,这样更容易被识别为测试类
  • 不需要继承自任何基类
  • 类中的每个测试方法仍需以 test_ 开头
1
2
3
4
5
6
7
8
9
# test_string_methods.py
class TestStringMethods:

def test_upper(self):
assert 'hello'.upper() == 'HELLO'

def test_isupper(self):
assert 'HELLO'.isupper()
assert not 'Hello'.isupper()

3、包:包名无特殊要求,但必须有 \_init_.py 文件

pytest 运行方式

主函数运行

1
2
3
4
5
6
7
import  pytest

def test_01():
print("啥也没有")

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

image.png

命令行运行

文件路径 testcase/test_one.py,在终端输入如下内容:

1
pytest ./testcase/test_one.py --html=./report/report.html

image.png

[!note] pytest 用例查找规则

命令 说明
pytest(等价于:python -m pytest) 运行当前目录及其所有子目录下的所有测试用例
pytest ./ 运行当前目录及其所有子目录下的所有测试用例(和上一个命令效果相同)
pytest case\sub_case 运行指定目录 case\sub_case 及子目录下的所有测试用例
pytest case\test_demo. py 只运行指定的测试文件 case\test_demo.py 中的所有测试用例
pytest -k test_2 运行所有测试函数或方法名包含 test_2 的测试用例;这个命令会筛选出所有名字中带有 test_2 的测试用例来运行
pytest case\test_demo.py::test_a 只运行文件 case\test_demo.py 中的特定测试函数 test_a
pytest case\test_demo.py::TestDemo 只运行文件 case\test_demo.py 中的特定测试类 TestDemo 内的所有测试方法
pytest case\test_demo.py::TestDemo1:: test_c 只运行文件 case/test_demo.py 中的特定测试类

pytest.ini 配置文件方式运行

pytest.ini 是 pytest 的主配置文件,可以添加配置改变 pytest 的默认行为,这样不用我们每次执行都在命令行中指定很多参数;此配置文件通常放到项目根目录下;配置文件样例:

file:pytest.ini
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[pytest]
# 命令行执行参数
addopts = -vs --strict-markers
# 排除目录
norecursedirs = xxx
# 默认执行目录
testpaths = case
# 执行规则:class
python_classes = Test*
# 执行规则:py文件
python_files = test_* *_test
# 执行规则:function
python_functions = test_*
# 自定义注册标记
markers =
user: 用户模块
order: 订单模块

以下是一些常用的配置选项以及解释:

1、addopts:用于设置命令行选项,这些选项将在每次运行 pytest 时自动添加

1
2
[pytest]
addopts = -v -s --maxfail=3
  • -v:详细模式
  • -s:显示打印输出
  • --maxfail=3:当有 3 个测试失败时停止运行

2、testpaths:用于指定测试文件或者目录的路径,pytest 会在这些路径下查找测试

1
2
[pytest]
testpaths = tests integration_tests

在这个示例中,pytest 会在 tests 和 integration_tests 目录下查找测试文件

3、norecursedirs:用于指定 pytest 排除查找的目录

1
2
[pytest]
norecursedirs = .git venv

在这个示例中,pytest 会忽略 .git 和 venv 目录

4、filterwarnings:用于配置警告过滤器

1
2
3
4
[pytest]
filterwarnings =
ignore::DeprecationWarning
ignore::UserWarning

在这个示例中,pytest 会忽略 DeprecationWarning 和 UserWarning 类型的警告

5、markers:用于定义自定义标记,对于分区不同类型的测试非常有用

1
2
3
4
[pytest]
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests

在这个示例中,定义了两个标记:slow 和 integration

6、timeout:用于设置测试的超过时间(需要 pytest-timeout 插件)

1
2
[pytest]
timeout=300

7、env:用于设置环境变量(需要 pytest-env 插件)

1
2
3
4
[pytest]
env =
ENV=staging
DEBUG=1

在这个示例中,配置了两个环境变量 ENV 和 DEBUG

8、xfail_strict:用于设置 xfail 标记是否严格,默认为 false

1
2
[pytest]
xfail_strict = true

在这个示例中,xfail 标记会严格对待,如果标记的测试意外通过,测试将失败。

9、junit_family:用于设置生成的 JUnit XML 报告的格式。

1
2
[pytest] 
junit_family = xunit2

在这个示例中,生成的 JUnit XML 报告将使用 xunit2 格式

pytest conftest.py 文件

conftest.py 特点:

  • pytest 会默认读取 conftest.py 里面的所有 fixture
  • conftest.py 文件名称是固定的,不能改动
  • conftest.py 只对同一个 package 下的所有测试用例生效
  • 不同目录可以有自己的 conftest.py,一个项目中可以有多个 conftest.py
  • 测试用例文件中不需要手动 import conftest.py,pytest 会自动查找

image.png

pytest fixture 装饰器

fixture 介绍

虽然 setup 和 teardown 可以执行一些前置和后置操作,但是这种是针对整个脚本全局生效的,如果有以下场景:

  1. 用例一需要执行登录操作;
  2. 用例二不需要执行登录操作;
  3. 用例三需要执行登录操作,则 setup 和 teardown 则不满足要求

fixture 的优势主要有如下四点:

  1. 与 setup、teardown 类似,fixture 提供了测试执行前和测试执行后的处理,但是又比 setup、teardown 更灵活好用,比如:fixture 命名更加灵活,不局限于 setup 和 teardown
  2. conftest.py 配置里可以实现数据共享,可以方便管理、修改和查看 fixture 函数,并且不需要 import 就能自动找到 fixture
  3. fixture 可用于封装数据,也可用于封逻辑动作,使用范围非常广

fixture 的调用方式:@pytest.fixture(scope = "function", params=None, autouse=False, ids=None, name=None)

fixture 参数 scope

作用:

  • 用于控制 fixture 的作用范围
  • 作用类似于 pytest 的 setup/teardown
  • 默认取值为 fuction,控制范围为 session > module > class > function

fixture 作用范围有四个:function、class、module 和 session

取值 范围说明
function 函数级:每一个函数或方法都会调用
class 函数级:模块级每一个.py 文件调用一次
module 模块级:每一个.py 文件调用一次
session 会话级:每次会话只需要运行一次,会话内所有方法及类,模块都共享这个方法

[!note] function

1、作为参数传递

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
import pytest
# fixture函数(类中) 作为多个参数传入
@pytest.fixture()
def login():
print("打开浏览器")
a = "account"
return a

@pytest.fixture()
def logout():
print("关闭浏览器")

class TestLogin:
#传入lonin fixture
def test_001(self, login):
print("001传入了loging fixture")
assert login == "account"

#传入logout fixture
def test_002(self, logout):
print("002传入了logout fixture")

def test_003(self, login, logout):
print("003传入了两个fixture")

def test_004(self):
print("004未传入仍何fixture哦")

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

image.png

从运行结果可以看出,fixture 作为参数传入时,会在执行函数之前执行该 fixture 函数;再将值传入测试函数作为参数使用,这个场景多用于登录

2、fixture 的相互调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pytest
# fixtrue作为参数,互相调用传入
@pytest.fixture()
def account():
a = "account"
print("第一层fixture")
return a

#Fixture的相互调用一定是要在测试类里调用这层fixture才会生次,普通函数单独调用是不生效的
@pytest.fixture()
def login(account):
print("第二层fixture")

class TestLogin:
def test_1(self, login):
print("直接使用第二层fixture,返回值为{}".format(login))

def test_2(self, account):
print("只调用account fixture,返回值为{}".format(account))


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

image.png

得出结论:

  1. 即使 fixture 之间支持相互调用,但普通函数直接调用 fixture 是不支持的,一定是在测试函数内调用才会逐级调用生效
  2. 有多层 fixture 调用时,最先执行的是最后一层 fixture,而不是先执行传入测试函数的fixture
  3. 上层 fixture 的值不会自动 return,这里就类似函数相互调用一样的逻辑

[!note] class

1、当测试类内的每一个测试方法都调用了 fixture,fixture 只在该 class 下所有测试用例执行前执行一次

2、测试类下面只有一些测试方法使用了 fixture 函数名,这样的话,fixture 只在该 class 下第一个使用 fixture 函数的测试用例位置开始算,后面所有的测试用例执行前只执行一次。而该位置之前的测试用例就不管

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pytest
@pytest.fixture(scope='class')
def login():
a = '123'
print("输入账号密码登陆")

class TestLogin:
def test_1(self):
print("用例1")

def test_2(self, login):
print("用例2")

def test_3(self, login):
print("用例3")

def test_4(self):
print("用例4")

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

image.png

[!note] moudule

与 class 相同,只从 .py 文件开始引用 fixture 的位置生效

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
import pytest  


@pytest.fixture(scope='module')
def login():
print("fixture范围为module")


def test_01():
print("用例01")


def test_02(login):
print("用例02")


class TestLogin():
def test_1(self):
print("用例1")

def test_2(self):
print("用例2")

def test_3(self):
print("用例3")


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

image.png

[!note] session

session 的作用范围是针对 .py 级别的,module 是对当前 .py 生效,session 是对多个 .py 生效;session只作用于一个 .py 文件时,作用相当于module,所以session多数与contest.py文件一起使用,做为全局 Fixture

fixture 参数 autouse

autocase 默认为 False;若为 True,则每个测试函数都会自动调用该 fixture,无需传入 fixture 函数名;由此我们可以总结出调用 fixture 的三种方式:

  1. 函数或类里面方法直接传fixture的函数参数名称
  2. 使用装饰器 @pytest.mark.usefixtures() 修饰
  3. autouse=True 自动调用,无需传仍何参数,作用范围跟着 scope 走
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pytest  


@pytest.fixture(autouse="true")
def login():
print("当 autocase 为 true")

class TestLogin:
def test_1(self):
print("用例1")
def test_2(self):
print("用例2")

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

image.png

fixture 参数 params

  • fixture 的可选形参列表,支持列表传入
  • 默认 None,每个 param 的值 fixture 都会去调用执行一次,类似 for 循环
  • 可与参数 ids 一起使用,作为每个参数的标识
  • 被 fixture 装饰的函数要调用是采用:Request.param
1
2
3
4
5
6
7
8
9
10
11
12
import pytest  


@pytest.fixture(params=[1,2,{'a':1,'b':2},{4,5,5}])
def demo(request):
return request.param

def test_demo(demo):
print("value{}".format(demo))

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

image.png

固定写法:在 pytest 中有一个内建的 fixture 叫做 request,代表 fixture 的调用状态。request 有一个字段 param,可以使用类似 @pytest.fixture(param=tasks_list) 的方式,在 fixture 中使用 request.param 的方式作为返回值供测试函数调用。其中 tasks_list 包含多少元素,该 fixture 就会被调用几次,分别作用在每个用到的测试函数上

fixture 参数 ids

用例标识 id,每个 ids 和 params 一一对应,如果没有 id,将从 params 自动产生

1
2
3
4
5
6
7
8
9
10
11
12
import pytest  


@pytest.fixture(params=[1,2,{'a':1,'b':2},{4,5,5}], ids=['a','b','c','d'])
def demo(request):
return request.param

def test_demo(demo):
print("value{}".format(demo))

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

image.png

fixture 参数 name

fixture 的重命名:通常来说使用 fixture 的测试函数会将 fixture 的函数名作为参数传递,但是 pytest 也允许将fixture重命名;如果使用了name,那只能将name传如,函数名不再生效

1
2
3
4
5
6
7
8
9
10
11
12
import pytest
@pytest.fixture(name="new_fixture")
def test_name():
pass

#使用name参数后,传入重命名函数,执行成功
def test_1(new_fixture):
print("使用name参数后,传入重命名函数,执行成功")

#使用name参数后,仍传入函数名称,会失败
def test_2(test_name):
print("使用name参数后,仍传入函数名称,会失败")

image.png

pytest 跳过测试用例

@pytest.mark.skip

跳过执行测试用例,有可选择参数 reason:跳过的原因,会在执行结果中打印

  • @pytest.mark.skip 可以加在函数上、类上、类方法上
  • 如果加在类上面,类里面所有的测试用例多不会执行
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
import pytest


@pytest.fixture(autouse=True)
def login():
print("====登录====")


def test_case01():
print("我是测试用例11111")


@pytest.mark.skip(reason="不执行该用例!!因为没写好!!")
def test_case02():
print("我是测试用例22222")


class Test1:

def test_1(self):
print("%% 我是类测试用例1111 %%")

@pytest.mark.skip(reason="不想执行")
def test_2(self):
print("%% 我是类测试用例2222 %%")


@pytest.mark.skip(reason="类也可以跳过不执行")
class TestSkip:
def test_1(self):
print("%% 不会执行 %%")

pytest.skip()

作用:在测试用例执行期间强制跳过不再执行剩余内容,类似于在 python 的循环中,满足某些条件则 break

1
2
3
4
5
6
7
def test_function():
n = 1
while True:
print(f"这是我第{n}条用例")
n += 1
if n == 5:
pytest.skip("我跑五次了不跑了")

pytest parametrize 参数化

pytest 允许在多个级别启用测试参数化

  1. pytest.fixture() 允许 fixture 有参数化功能
  2. pytest.mark.parametrize 允许在测试函数和类中定义多组参数和 fixtures
  3. pytest_generate_tests 允许定义自定义参数化方案或扩展

基本介绍

方法:parametrize(argnames, argvalues, indirect=False, ids=None, scope=None)

常用参数:

1、argnames:参数名,格式为:”arg1,arg2,arg3,…”,通过逗号分隔多个参数

1
2
3
4
5
6
7
8
9
# 多参数写法汇总:参数名可以是字符串、元组、列表、字符串放元组中

@pytest.mark.parametrize("input,expected", [("1+1", 2), ("2-4", -2), ("2*3", 6)])

@pytest.mark.parametrize(("input","expected"), [("1+1", 2), ("2-4", -2), ("2*3", 6)])

@pytest.mark.parametrize(["input","expected"], [("1+1", 2), ("2-4", -2), ("2*3", 6)])

@pytest.mark.parametrize(("input,expected"), [("1+1", 2), ("2-4", -2), ("2*3", 6)])

2、argvalues:参数对应值,类型必须为 list;

  • 当参数为一个时格式:[v1]
  • 当参数个数大于一个时,格式为:[(v1_1, v2_1, …), (v1_2, v2_2, …)],一组参数值放元组或者列表中,也就是说,最外层列表可以嵌套元组或者列表
1
2
3
@pytest.mark.parametrize("name,technology",[['韧','测试开发'],['全栈测试笔记','性能测试']])  # 列表嵌列表

@pytest.mark.parametrize("name,technology",[('韧','测试开发'),('全栈测试笔记','性能测试')]) # 列表嵌套元组

3、indirect:默认是 False,如果设置成 True,表示把被 parametrize 修饰器修饰的方法形参当函数执行(parametrize 中参数名和这个形参同名);同时,必须有这个函数,且被 @pytest.fixture() 修饰,否则报错:fixture ‘xxx’ not found,xxx 表示形参名

4、ids:用例 id,用于标识用例,增加可读性(测试结果中会展示 id),是字符串列表,ids 的长度要与测试数据列表长度一致

具体使用

单个参数的参数化

参数方法形参名要和 parametrize 里面的参数一样

1
2
3
4
@pytest.mark.parametrize("data", ['韧', '全栈测试笔记', '性能测试', '自动化测试', '测试开发'])  
class TestQzcsbj:
def test_case(self, data):
print(f"data={data}")

image.png

多个参数的参数化

参数放列表中,且列表嵌套元组或者列表

示例一:修饰器放函数上,参数值是基本类型

1
2
3
@pytest.mark.parametrize("input,expected", [("1+1", 2), ("2-4", -2), ("2*3", 6)])  
def test_eval(input, expected):
assert eval(input) == expected

image.png

示例二:修饰器放函数上,参数值是字典

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

data = [
{
"input": "1+1",
"expected": 2
},
{
"input": "2-4",
"expected": -2
},
{
"input": "2*3",
"expected": 6
}
]


@pytest.mark.parametrize("par", data)
def test_eval(par):
print(f"本次入参是:input={par['input']}, expected={par['expected']}")
assert eval(par['input']) == par['expected']

image.png

示例三:修饰器放测试类上

1
2
3
4
5
@pytest.mark.parametrize("name,technology",[['韧','测试开发'],['全栈测试笔记','性能测试']])  # 列表嵌列表  
# @pytest.mark.parametrize("name,technology", [('韧', '测试开发'), ('全栈测试笔记', '性能测试')]) # 列表嵌套元组
class TestQzcsbj:
def test_case(self, name, technology):
print(f"name={name}, technology={technology}")

image.png

示例四:测试类下多个方法,会将测试数据传给此类下所有测试方法

1
2
3
4
5
6
7
8
# @pytest.mark.parametrize("name,technology",[['韧','测试开发'],['全栈测试笔记','性能测试']])  # 列表嵌列表  
@pytest.mark.parametrize("name,technology", [('韧', '测试开发'), ('全栈测试笔记', '性能测试')]) # 列表嵌套元组
class TestQzcsbj:
def test_case1(self, name, technology):
print(f"test_case1: name={name}, technology={technology}")

def test_case2(self, name, technology):
print(f"test_case2: name={name}, technology={technology}")

image.png

参数放变量中

1
2
3
4
5
6
7
8
9
import pytest  

data = [('韧', '测试开发'), ('全栈测试笔记', '性能测试')]


@pytest.mark.parametrize("name,technology", data)
class TestQzcsbj:
def test_case(self, name, technology):
print(f"name={name}, technology={technology}")

image.png

调用函数获取参数数据

1
2
3
4
5
6
7
8
9
10
11
import pytest  


def get_data():
return [('韧', '测试开发'), ('全栈测试笔记', '性能测试')]


@pytest.mark.parametrize("name,technology", get_data())
class TestQzcsbj:
def test_case(self, name, technology):
print(f"name={name}, technology={technology}")

image.png

mark 标记参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pytest  


@pytest.mark.parametrize(
"input,expected",
[
("1+1", 2),
pytest.param("2-4", -2, marks=[pytest.mark.minus, pytest.mark.xfail], id="input:2-4"),
pytest.param("2*3", 6, marks=[pytest.mark.multiplication, pytest.mark.skip], id="input:2*3"),
pytest.param("4/2", 2, marks=pytest.mark.division, id="input:4/2")
]
)
def test_eval(input, expected):
assert eval(input) == expected

添加配置:

1
2
3
4
5
6
[pytest]
addopts = -s --strict-markers
markers =
minus
multiplication
division

image.png

笛卡尔积:多个 parametrize 参数化修饰器

1
2
3
4
5
6
import pytest
@pytest.mark.parametrize("username",["qzcsbj","ren","jack"])
@pytest.mark.parametrize("password",["a123","abc123","ABC123"])
class TestQzcsbj:
def test_case(self, username, password):
print(f"name={username}, technology={password}")

image.png

笛卡尔积:parametrize 与 fixture 混合

1
2
3
4
5
6
7
8
9
10
11
import pytest  


@pytest.fixture(params=["qzcsbj", "ren", "jack"])
def fun(request):
return request.param


@pytest.mark.parametrize("password", ["a123", "abc123", "ABC123"])
def test_case(fun, password):
print(f"name={fun}, password={password}")

image.png

parametrize 数据驱动

数据驱动就是通过数据的改变从而驱动自动化测试的执行,最终引起测试结果的改变。简单来说,就是参数化的应用。

数据量小的测试用例可以使用代码的参数化来实现数据驱动,数据量大的情况下建议使用一一种结构化的文件(例如 yaml、json 等) 来对数据进行存储,然后在测试用例中读取这些数据。

但是,建议不管数据多少,都要数据和代码的分离,方便维护

parametrize 参数化数据来自 yaml 文件

file:case.yaml
1
2
3
4
- uname: ren
pwd: 123
- uname: qzcsbj
pwd: 456
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import pytest
import yaml
import os

# 获取项目路径
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

def read_data_from_yaml(file_path):
f = open(file_path, "r", encoding="utf-8")
res = yaml.load(f, yaml.FullLoader)
f.close()
print(f"返回的参数:{res}")
return res

@pytest.mark.parametrize("param", read_data_from_yaml(BASE_PATH+"/data/case.yaml"))
def test_case(param):
print(f"uname={param['uname']}, pwd={param['pwd']}")

image.png

parametrize 参数化数据来自 json 文件

file:case.json
1
2
3
4
5
6
7
8
9
10
[
{
"uname": "ren",
"pwd": "123"
},
{
"uname": "qzcsbj",
"pwd": "456"
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import pytest
import json
import os

# 获取项目路径
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

def read_data_from_json(file_path):
data = {}
with open(file_path, 'r', encoding="utf-8") as fp:
data = json.load(fp)
print(f"返回的参数:{data}")
return data

@pytest.mark.parametrize("param", read_data_from_json(BASE_PATH+"/data/case.json"))
def test_case(param):
print(f"uname={param['uname']}, pwd={param['pwd']}")

parametrize 参数化数据来自 excle 文件

parametrize 参数化数据来自 csv 文件

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import pytest
import csv
import os

# 获取项目路径
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

def read_data_from_csv(file_path):
item = []
c = csv.reader(open(file_path, "r", encoding='utf-8'))
for i in c:
item.append(i)
print(f"返回的参数:{item}")
return item

@pytest.mark.parametrize("param", read_data_from_csv(BASE_PATH+"/data/case.csv"))
def test_case(param):
print(f"uname={param[0]}, pwd={param[1]}")

pytest 标记测试用例

[!note] 通常,我们通过分包或者分模块来对用例进行分类管理,如果只想执行符合某要求的部分用例,可以使用装饰器 @pytest.mark.xxx 给用例打标签(自定义标记)

自定义标记使用流程:

  1. 注册自定义标记(通过 pytest.ini 管理)
  2. 将模块、函数、类、方法进行业务标记
  3. 根据自定义标记运行用例

注册自定义标记

在 pytest.ini 文件中,新建标记:

1
2
3
4
5
6
7
[pytest]
markers =
user: user marker
product: product marker
findproduct
deleteproduct
modulex: modulex marker
  1. 标记名要是英文命名,建议参考模块命名,要有业务含义
  2. 所有自定义标记,建议在 pytest.ini 中进行统一管理,并通过命令参数 –strict-markers 进行授权(非必须)
  3. pytest 中的 markers 配置相当于我们对用例的一种归类
1
2
3
4
5
6
7
8
[pytest]
addopts = --strict-markers
markers =
user: user marker
product: product marker
findproduct
deleteproduct
modulex: modulex marker
  • 开启严格标记,如果标记不在配置文件中,会报错
  • 不开启严格标记,如果标记不在配置文件中,会 warning

获取现有标记:pytest --markers,包含自定义和内置

自定义标记分类

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
import pytest  

pytestmark = pytest.mark.modulex # 模块类标记
# pytestmark = [pytest.mark.modulex, pytest.mark.moduley] # 可以注册多个

@pytest.fixture(params=['韧', '全栈测试笔记', '性能测试'])
def fun(request): # 必须是request这个参数名
return request.param # 依次取列表中的每个值返回


@pytest.fixture(params=['apple', 'car'])
def fun2(request):
return request.param


@pytest.mark.user # 函数标记
def test_user_reg(fun):
print(f"---test_user_reg: {fun}")


@pytest.mark.user
def test_user_login():
print("---test_user_login")


@pytest.mark.product # 类标记
class TestProduct:
@pytest.mark.findproduct # 方法标记
def test_product_find(self, fun2):
print(f"---test_product_find: {fun2}")

@pytest.mark.deleteproduct
def test_product_delete(self):
print("---test_product_delete")
  • 模块、函数、方法、类上都可以标记多个
  • 模块比较的方式是:pytestmark = [pytest.mark.modulex, pytest.mark.moduley]
  • 函数、方法、类标记方式是:@pytest.mark.module1

pytest 标记失败函数和失败重试

标记为预期失败函数

方法:xfail(condition=None, reason=None, raises=None, run=True, strict=False)

常用参数:

  • condition:预期失败的条件,必传参数
  • reason:失败的原因,必传参数

使用方式:@pytest.mark.xfail(condition, reason="xx")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pytest  


class Test_ABC:
def setup_class(self):
print("------->setup_class")

def teardown_class(self):
print("------->teardown_class")

def test_a(self):
print("------->test_a")
assert 1

# 标记为预期失败函数test_b
@pytest.mark.xfail(2 > 1, reason="标注为预期失败")
def test_b(self):
print("------->test_b")

assert 0

失败后重试

需要安装第三方插件:pytest-return、pytest-returnfailures

pytest 调整执行顺序

pytest 设置断点

在用例脚本中加入如下 python 代码,pytest会自动关闭执行输出的抓取,这里,其他test脚本不会受到影响,带断点的test上一个test正常输出

1
import pdb; pdb.set_trace()
1
2
3
4
5
def test_example():
x = 1
y = 2
import pdb; pdb.set_trace() # 这里设置断点
assert x + y == 3

在运行上面的代码时,pytest 会在 pbd.set_trace() 处暂停,进入调试模式,可以在此处检查变量 x 和 y 的值

pytest 获取用例执行性能

获取最慢的 10 个用例的耗时:pytest --durations=10

pytest 断言

在 pytest 中,断言(assertions)是用于验证测试结果是否符合预期的关键部分。断言允许你检查某个条件是否为真,如果条件为假,测试将失败并报告错误。

基本断言

pytest 最基本的断言形式是使用 Python 内置的 assert 语句,pytest 会捕获这些断言,并在断言失败时提供详细的错误信息

1
2
3
def test_simple_assertion():
result = 1 + 1
assert result = 2

image.png

复杂断言

pytest 支持复杂的断言,包括对集合、字典、对象等的检查

1
2
3
4
5
def test_complex_assertion():
data = {"key1": "value1", "key2": "value2"}
assert data["key1"] == "value1"
assert "key2" in data
assert data == {"key1": "value1", "key2": "value2"}

image.png

自定义错误信息

可以在断言中添加自定义的错误信息以便在断言失败时提供更多的上下文信息

1
2
3
def test_custom_error_message():
result = 1 + 1
assert result == 3, f"Expected 3 but got {result}"

image.png

断言异常

pytest 提供了 pytest.raises 上下文管理器,用于断言某个代码块会抛出特定的异常

1
2
3
4
5
import pytest

def test_exception_assertion():
with pytest.raises(ZeroDivisionError):
result = 1 / 0

image.png

在这个示例中,pytest.raises(ZeroDivisionError) 确保代码块 result = 1 / 0 会抛出 ZeroDivisionError 异常;如果异常没有抛出,测试将失败

断言警告

pytest 还提供了 pytest.warns 上下文管理器,用于断言某个代码块会抛出特定的警告

1
2
3
def test_warning_assertion():
with pytest.warns(UserWarning):
warnings.warn("This is a warning", UserWarning)

image.png

在这个示例中,pytest.warns(UserWarning) 确保代码块 warnings.warn("This is a warning", UserWarning) 会抛出 UserWarning 警告;如果代码快没有警告,那么就会报错

断言属性

pytest 支持对对象属性的断言,可以使用 hasattr、getattr 等内置函数

1
2
3
4
5
6
7
8
class SampleClass:
def __init__(self):
self.attribute = "value"

def test_attribute_assertion():
obj = SampleClass()
assert hasattr(obj, "attribute")
assert getattr(obj, "attribute") == "value"

在这个示例中,断言检查了对象 obj 是否具有 attribute 属性,并且该属性的值是否为 “value”

pytest 异常处理

上文讲解断言时提到了断言异常,即为异常处理,在 pytest 中,异常处理允许编写测试来验证代码是否按照预期抛出异常;pytest 提供了 pytest.raises 上下文管理器用于捕获和校验异常

基本用法

pytest.raises 上下文管理器用于捕获特定的异常,并在异常未抛出时使测试失败

1
2
3
def test_division():  
with pytest.raises(ZeroDivisionError):
result = 1 / 0

在这个示例中,pytest.raises(ZeroDivisionError) 确保代码块 result = 1 / 0 会抛出 ZeroDivisionError 异常;如果异常没有抛出,测试将失败

获取异常信息

可以通过 pytest.raises 的 match 参数来验证异常消息的内容

1
2
3
def test_exception_message():
with pytest.raises(ValueError, match="invalid literal for int()"):
int("not a number")

在这个示例中,pytest.raises(ValueError, match="invalid literal for int()") 确保抛出的 ValueError 异常的消息包含 "invalid literal for int()"

访问异常对象

可以通过 pytest.raises 的 as 语法来捕获异常对象,并对其进行进一步的检查

1
2
3
4
def test_exception_object():
with pytest.raises(ValueError) as exc_info:
int("not a number")
assert str(exc_info.value) == "invalid literal for int(): not a number"

在这个示例中,exc_info 是一个 ExceptionInfo 对象,包含了捕获的异常信息。你可以通过 exc_info.value 访问异常对象,并进行断言

结合固件使用

pytest.raises 可以与固件(fixtures)结合使用,以确保在特定的测试上下文中抛出异常

1
2
3
4
5
6
7
8
9
import pytest

@pytest.fixture
def setup_data():
return "not a number"

def test_exception_with_fixture(setup_data):
with pytest.raises(ValueError):
int(setup_data)

在这个示例中,setup_data 固件提供了测试数据,pytest.raises(ValueError) 确保在将 setup_data 转换为整数时抛出 ValueError 异常

嵌套异常处理

pytest.raises 可以嵌套在其他上下文管理器中,以处理更复杂的异常场景

1
2
3
4
def test_nested_exception():
with pytest.raises(ZeroDivisionError):
with pytest.warns(UserWarning):
result = 1 / 0

在这个示例中,pytest.raises(ZeroDivisionError) 确保 result = 1 / 0 抛出 ZeroDivisionError 异常,而 pytest.warns(UserWarning) 确保在执行代码块时抛出 UserWarning 警告

pytest 配置过滤警告

关于警告:如果警告不重要,可以忽略;如果警告很重要,可以升级为异常

配置过滤警告

1、命令行参数,pytest case\test_qzcsbj.py -vs -W error::UserWarning,表示将 UserWarning 警告转换为错误

2、pytest.ini 配置文件:表示将 UserWarning 警告转换为错误,其它忽略

file:pytest.ini
1
2
3
4
[pytest]
filterwarnings =
ignore
error::UserWarning

装饰器 filterwarnings 过滤

我们可以使用 @pytest.mark.filterwarnings 向特定测试项添加警告筛选器,这样可以做到更细节的控制警告

1、函数、方法级过滤

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

import pytest


# @pytest.mark.filterwarnings("ignore:.*自定义.*")
@pytest.mark.filterwarnings("ignore::UserWarning")
def test_a():
print("---test_a")
assert fun()==1

def fun():
print("---fun")
warnings.warn(UserWarning("自定义 warning"))
return 1

2、类级过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
import warnings

import pytest
@pytest.mark.filterwarnings("ignore:自定义")
class Test01:
def test_a(self):
print("---test_a")
assert self.fun()==1

def fun(self):
print("---fun")
warnings.warn(UserWarning("自定义 warning"))
return 1

3、模块级过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import warnings
import pytest

# pytestmark = pytest.mark.filterwarnings("ignore:自定义")
pytestmark = pytest.mark.filterwarnings("ignore")

class Test01:
def test_a(self):
print("---test_a")
assert self.fun()==1

def fun(self):
print("---fun")
warnings.warn(UserWarning("自定义 warning"))
return 1