一、pytest入门
1.简介
- pytest是Python的一个单元测试框架,与python自带的unittest测试框架类似;
- pytest比unittest框架使用起来更简洁,效率更高,而且特性比较多,也就非常的灵活;
pytest比unittest框架区别:
- 语法和风格:
-
pytest
的语法更简洁和灵活,测试函数不需要继承特定的类,只需要以test_
开头即可。unittest
要求测试用例继承自unittest.TestCase
类,测试方法必须以test
开头。
- 断言方式:
-
pytest
可以使用标准的 Python 断言语句,更直观和简洁。unittest
则需要使用特定的断言方法,如assertEqual
、assertTrue
等。
- fixtures(测试夹具):
-
pytest
中的 fixtures 功能更强大和灵活,可以通过参数自动注入,并且可以有更复杂的范围控制(如function
、class
、module
、session
)。unittest
也有类似的 setUp 和 tearDown 方法,但功能相对较简单。
- 参数化:
-
pytest
的参数化更简单直观,使用@pytest.mark.parametrize
装饰器。unittest
实现参数化相对复杂。
- 插件生态:
-
pytest
拥有丰富的插件生态系统,可以方便地扩展其功能。unittest
的插件相对较少。
- 执行方式:
-
pytest
可以自动发现测试用例,无需显式指定测试模块。unittest
通常需要在测试脚本中明确指定要运行的测试用例。
- 错误报告:
-
pytest
的错误报告通常更详细和易于理解。
- 比如pytest常用的特性有:
① 对case可以进行设置跳过,也可以进行标记(比如失败等);
② 可以重复执行失败的case;
③ 可以兼容执行unittest编写的case;
④ 有很多第三方的插件,比如报告allure等;
⑤ 支持持续集成;
⑥ 和unittest一样支持参数化,但Pytest方法更多,更灵活;
2.官方文档
Full pytest documentation - pytest documentation
3.pytest框架约束
- 模块名(py文件)必须是以test_开头或者_test结尾
- 测试类(class)必须以Test开头,并且不能带init方法,类里的方法必须以test_开头
- 测试用例(函数)必须以test_开头
4.pytest运行方式
4.1 主函数运行
import pytest
def test_01():
print('这个是测试用例一')
assert 1 == 1
if __name__ == '__main__':
pytest.main([])
4.2 命令行运行
4.3 main中可用参数
-v 输出信息
- 如:文件名/用例名信息
if __name__ == '__main__':
pytest.main(["-v"])
-s 输出调试信息
- 如:打印信息
if __name__ == '__main__':
pytest.main(["-s"])
-x 用例失败停止执行
- 只要有一个用例执行失败,就停止执行测试
import pytest
def test_01():
assert 1 == 1
print('这是成功用例')
def test_02():
assert 2 == 1
def test_03():
assert 2 == 3
def test_04():
assert 4 == 4
if __name__ == '__main__':
pytest.main(["-vs", "-x"])
- --maxfail=N 出现N个测试用例失败,就停止测试
import pytest
def test_01():
assert 1 == 1
print('这是成功用例')
def test_02():
assert 2 == 1
def test_03():
assert 2 == 3
def test_04():
assert 4 == 4
if __name__ == '__main__':
pytest.main(["-vs", "--maxfail=2"])
-m 通过标记表达式执行
import pytest
@pytest.mark.wjh
def test_01():
assert 1 == 1
print('这是成功用例')
def test_02():
assert 2 == 1
def test_03():
assert 2 == 3
def test_04():
assert 4 == 4
if __name__ == '__main__':
pytest.main(["-vs", "-m wjh"])
-k 指定用例执行
import pytest
def test_01():
assert 1 == 1
print('这是成功用例')
def test_02():
assert 2 == 1
class TestCls:
def test1_02(self):
assert 2 == 3
def test_14(self):
assert 4 == 4
- -k
if __name__ == '__main__':
pytest.main(["-vs", "-k _02"])
- -k or
if __name__ == '__main__':
pytest.main(["-vs", "-k test_01 or test_02"])
- -k and
if __name__ == '__main__':
pytest.main(["-vs", "-k test_ and 02"])
- -k and not
if __name__ == '__main__':
pytest.main(["-vs", "-k test_ and not 02"])
4.4 ini配置文件运行
参数 | 作用 |
[pytest] | 用于标志这个文件是pytest的配置文件 |
addopts | 命令行参数,多个参数之间用空格分隔 |
testpaths | 配置搜索参数用例的范围 |
python_files | 改变默认的文件搜索规则 |
python_classes | 改变默认的类搜索规则 |
python_functions | 改变默认的测试用例的搜索规则 |
markers | 用例标记,自定义mark,需要先注册标记,运行时才不会出现warnings |
filterwarnings | 配置运行用例时忽略的警告信息 |
[pytest]
addopts = -vs -m
testpaths =
test_scenario
python_files = test*.py
;配置测试搜索的测试类名
python_classes = Test*
;配置测试搜索的测试函数名
python_functions = test
markers =
wjh: 'wjh标签'
filterwarnings =
ignore::pytest.PytestAssertRewriteWarning
ignore::UserWarning
ignore::RuntimeWarning
ignore::DeprecationWarning
5.插件
控制用例执行顺序
- 需下载pytest-ordering插件,数字越小越靠前执行
import pytest
@pytest.mark.run(order=2)
def test_01():
print('这个是测试用例一')
assert 1 == 1
@pytest.mark.run(order=1)
def test_02():
print('这个是测试用例二')
assert 2 == 2
if __name__ == '__main__':
pytest.main(['-vs'])
控制失败用例重跑
- 需下载pytest-rerunfailures插件。使用 --reruns nums(nums代表重跑次数)
import pytest
def test_01():
print('这个是测试用例一')
assert 1 == 1
def test_02():
print('这个是测试用例二')
assert 1 == 2
if __name__ == '__main__':
pytest.main(['-vs'])
控制用例多线程执行
- -n 分布式运行测试用例-需下载xdist插件
import pytest
def test_01():
assert 1 == 1
print('这是成功用例')
def test_02():
assert 2 == 2
def test_03():
assert 3 == 3
def test_04():
assert 4 == 4
if __name__ == '__main__':
pytest.main(['-n=2'])
6.断言:assert
import pytest
def test_01():
print('这个是测试用例一')
assert 1 == 1
def test_02():
print('这个是测试用例二')
assert 1 == 2
if __name__ == '__main__':
pytest.main(['-vs'])
- 断言分为接口断言和业务断言两种
import pytest
import requests
def test_01():
res = requests.post(url='https://test-zjyth.youmatech.com/n0/api/common-login/auth/public_key')
assert res.status_code == 200 # 接口断言
response = res.json()
assert response.get('code') == 3001 # 业务断言
assert response.get('msg') is None # 业务断言
if __name__ == '__main__':
pytest.main([''])
断言 | 说明 |
assert a==b | 判断a等于b |
assert a!=b | 判断a不等于b |
assert a>b | 判断a大于b |
assert a<b | 判断a小于b |
assert a in b | 判断b包含a |
assert a not in b | 判断b不包含a |
assert a is true | 判断a为真 |
assert a is false | 判断a为假 |
assert a is None | 判断a为空 |
assert a is not None | 判断a不为空 |
7.基于pytest框架自身的前置后置
- 一个完整的单元测试用例并不是仅仅有测试执行就可以的,还需要执行测试之前的前置工作以及执行测试之后的后置工作
在类以外使用的前置与后置
setup_module()/teardown_module()
setup_function()/teardown_function()
import pytest
def setup_module():
print("在整个模块前运行一次")
def teardown_module():
print("在整个模块后运行一次")
def setup_function():
print("在每个测试函数前运行一次")
def teardown_function():
print("在整个测试函数后运行一次")
def test_01():
print('这个是测试用例一')
def test_02():
print('这个是测试用例一')
if __name__ == '__main__':
pytest.main(['-vs'])
在类以内使用的前置与后置
setup_class()/teardown_class()
setup_method()/teardown_method()
import pytest
def setup_module():
print("在整个模块前运行一次")
def teardown_module():
print("在整个模块后运行一次")
def setup_function():
print("在每个测试函数前运行一次")
def teardown_function():
print("在整个测试函数后运行一次")
def test_01():
print('这个是测试用例一')
def test_02():
print('这个是测试用例二')
class TestCase:
@staticmethod
def setup_class():
print('在整个测试类前执行一次')
@staticmethod
def teardown_class():
print('在整个测试类后执行一次')
@staticmethod
def setup_method():
print('类里面每个测试方法前执行一次')
@staticmethod
def teardown_method():
print('类里面每个测试方法后执行一次')
def test_03(self):
print('这个是测试用例三')
def test_04(self):
print('这个是测试用例四')
if __name__ == '__main__':
pytest.main(['-vs'])
二、pytest中fixture装饰器
1.什么是fixture
Fixture
可以被看作是测试用例的一种辅助工具,它可以提供测试所需的各种资源,例如数据库连接、文件内容、模拟对象等。通过使用 Fixture
,可以将测试用例与这些复杂的准备和清理工作分离,使测试代码更加简洁、可维护和可重复使用。
2.Fixture的优势
- 命名方式灵活,不限于setup和teardown两种命名
- conftest.py可以实现数据共享,不需要执行import 就能自动找到fixture
- scope=module,可以实现多个.py文件共享前置
- scope=“session” 以实现多个.py 跨文件使用一个 session 来完成多个用例
3.Fixture调用方式
@pytest.fixture(scope='function', params=None, autouse=False, ids=None, name=None)
def test_02():
print('这个是测试用例二')
assert 2 == 2
4.作用范围
scope取值 | 范围说明 |
function | 一个函数调用一次 |
class | 一个类调用一次 |
module | 整个.py文件调用一次 |
package | 整个文件夹调用一次 |
session | 全局调用一次 |
5.Fixture参数说明
5.1 scope:作用域
- scope='function' 方法级,每个方法执行一次
import pytest
@pytest.fixture()
def setup_01():
print('这个是测试前置1')
class TestCase01:
def test_01(self, setup_01):
print('这个是测试用例1')
def test_02(self, setup_01):
print('这个是测试用例2')
- scope='class' 类级,每个类执行一次
import pytest
class TestCase01:
def test_01(self, setup):
print('这个是测试用例1')
def test_02(self, setup):
print('这个是测试用例2')
class TestCase02:
def test_01(self, setup):
print('这个是测试用例3')
def test_02(self, setup):
print('这个是测试用例4')
if __name__ == '__main__':
pytest.main(['-vs'])
- scope='module' 模块级,每个模块执行一次
- scope='package' 包级,每个包执行一次
- scope='session' 会话级,每此会话执行一次
- 不写默认 'function'
5.2.autouse
自动运行,默认是False
5.3.params和ids
params和自定义参数ids在参数化中介绍使用
5.4.name
给fixture起别名。
6.Fixture手动调用
- 方法一:通过传参实现
讲fixture传入方法中,在运行该方法用例之前,会先自动运行该fixture
import pytest
@pytest.fixture()
def setup_01():
print('这个是测试前置1')
class TestCase01:
def test_01(self, setup_01):
print('这个是测试用例1')
def test_02(self, setup_01):
print('这个是测试用例2')
- 方法二:通过@pytest.mark.usefixtures装饰器实现
# @Time : 2024/7/22 20:09
# @Author : Jacky Wang
# @File : test_04.py
import pytest
@pytest.fixture()
def setup_01():
print('这个是测试前置1')
class TestCase01:
@pytest.mark.usefixtures('setup_01')
def test_01(self):
print('这个是测试用例1')
@pytest.mark.usefixtures('setup_01')
def test_02(self):
print('这个是测试用例2')
if __name__ == '__main__':
pytest.main(['-vs'])
7.基于装饰器的一种特殊的前置后置
import pytest
@pytest.fixture()
def setup_and_teardown():
print("这个是测试前置")
yield
print("这个是测试后置")
def test_01(setup_and_teardown):
print('这个是测试用例一')
def test_02(setup_and_teardown):
print('这个是测试用例二')
if __name__ == '__main__':
pytest.main(['-vs'])
8.@pytest.mark.skip/@pytest.mark.skipif跳过用例
- @pytest.mark.skip
import pytest
def test_01():
print('这个是测试用例一')
assert 1 == 1
@pytest.mark.skip(reason='失败用例不执行')
def test_02():
print('这个是测试用例二')
assert 1 == 2
if __name__ == '__main__':
pytest.main(['-vs'])
- @pytest.mark.skipif
import pytest
def test_01():
print('这个是测试用例一')
assert 1 == 1
@pytest.mark.skipif(condition=True, reason='条件为真则不执行')
def test_02():
print('这个是测试用例二')
assert 1 == 2
if __name__ == '__main__':
pytest.main(['-vs'])
三、conftest.py
1.conftest
1.1 什么是conftest.py
可以理解成一个专门存在fixture的配置文件
1.2 实际开发场景
例如:多个测试用例文件(test_*.py)的所有用例都需要用登录功能作为前置操作,因此就不能把登录功能写到某个用例文件中
1.3 conftest.py的特点
- pytest会默认读取conftest.py里面的所有fixture
- conftest.py文件名称是固定的,不能改动
- conftest.py只对同一个package下的所有测试用例生效
- 不同目录可以有自己的conftest.py,一个项目中可以有多个conftest.py
- 测试用例文件中不需要手动import conftest.py,pytest会自动查找
1.4 conftest作用域
- session作用域级别conftest
- package作用域级别conftest
- module作用域级别conftest
- class作用域级别conftest
- function作用域
四、参数化
1.什么是参数化
在测试框架(如 pytest)中,可以使用参数化来运行同一个测试函数多次,每次使用不同的输入参数,从而覆盖更多的测试场景
2.参数化的实战使用
- @pytest.mark.parametrize() 装饰器接收两个参数
- 第一个参数是以字符串的形式标识用例函数的参数
- 第二个参数是以可迭代对象的形式传递测试数据,一般使用list或tuple
import requests
import pytest
def base_data() -> list:
return [
(
"test",
"wangjunhong",
"123456",
3001
),
(
"test",
"wangjunhong",
"1234567",
3001
),
(
"test",
"wangjunhong1",
"123456",
3001
)
]
class TestLogin:
@pytest.mark.parametrize("businessCode, loginName, password, expect", base_data())
def test_pc_login(self, businessCode, loginName, password, expect):
url = 'https://test.i.youmatech.com/api/login/p/submit'
data = {
"businessCode": businessCode,
"loginName": loginName,
"password": password,
}
headers = {"Content-Type": "application/json"}
res = (requests.post(url=url, headers=headers, json=data)).json()
assert res.get('code') == expect
- @pytest.mark.parametrize:ids
- 通过上面的执行结果我们不难看出,用例是通过将参数进行拼接的形式来展示的,如果参数不多时,我们还比较容易看出;如果参数过多时,你还能很明显的看出此条用例期望的执行结果吗?
- 此时我们将会使用另一个参数ids来自定义显示结果:
ids
必须是与数据数量相同的名称(必须是字符串)列表
# @Time : 2024/7/22 20:09
# @Author : Jacky Wang
# @File : test_04.py
# encoding='utf-8'
import requests
import pytest
def base_data() -> tuple:
return (
(
"test",
"wangjunhong",
"123456",
3001
),
(
"test",
"wangjunhong",
"1234567",
3001
),
(
"test",
"wangjunhong1",
"123456",
3001
)
)
class TestLogin:
@pytest.mark.parametrize("businessCode, loginName, password, expect", base_data(),
ids=["login_success", 'login_fail', 'login_fail'])
def test_pc_login(self, businessCode, loginName, password, expect):
url = 'https://test.i.youmatech.com/api/login/p/submit'
data = {
"businessCode": businessCode,
"loginName": loginName,
"password": password,
}
headers = {"Content-Type": "application/json"}
res = (requests.post(url=url, headers=headers, json=data)).json()
assert res.get('code') == expect
# class TestGetName:
#
# @pytest.mark.parametrize("name", ['张三', '李四', '王五'])
# def test_get_name(self, name):
# print(name)
#
#
# class TestGetNameAndAge:
#
# @pytest.mark.parametrize("name, age", [('张三', 20), ('李四', 30), ('王五', 40)])
# def test_get_name(self, name, age):
# print(name, age)
if __name__ == '__main__':
pytest.main(['-vs'])
五、持续集成
1.pytest+jenkins+allure
略