Pytest 基础用法

1
2
3
4
5
# conda 
conda install pytest

# python
pip3 install pytest

Pytest 用例命名规范

  1. 测试文件必须以 “test_” 开头(或者 “_test” 结尾)

    • 如果直接运行文件,有目的文件,文件名不需要添加 test_ 关键字

    • 如果运行目录,目录下的运行文件必须以 “test_” 开头(或者 “_test” 结尾)

  2. 测试方法必须以 “test_” 开头

  3. 测试类必须以 Test 开头,并且不能有 init 方法

Pytest 用例执行顺序

  1. 默认执行顺序

    • 文件内:根据代码顺序执行
    • 文件夹内:根据默认文件顺序执行

    注意:unittest 默认执行顺序是按字符顺序执行

  2. 使用 pytest-ordering 自定义顺序

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
# 安装
conda install pytest-ordering
pip install pytest-ordering

# 示例:
class TestOrder:
"""
使用 pytest-ordering 自定义顺序
"""

@pytest.mark.run(order=1)
def test_login(self):
print("登录...")

@pytest.mark.run(order=4)
def test_pay(self):
print("Pay...")

@pytest.mark.run(order=2)
def test_search(self):
print("Search...")

@pytest.mark.run(order=3)
def test_order(self):
print("Order...")

Pytest 常用断言类型

  • 等于: ==
  • 不等于:!=
  • 大于:>
  • 小于:<
  • 属于:in
  • 不属于:not in
  • 大于等于:>=
  • 小于等于:<=
  • 是:is
  • 不是:is not

Pytest + Requests

  • 执行 pytest -vs .\test_request\test_request.p
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
def test_phone_number_with_get():
url = "https://api.binstd.com/shouji/query"
headers = {
'Content-Type': 'application/json;charset=UTF-8',
}

payload = {
'shouji': '16638902000',
'appkey': '2528a4a09cea4224',
}

result = Session.get(url=url, headers=headers, params=payload, verify=False)
print(result.json())
results = result.json()
demo = {
"status": "0",
"msg": "ok",
"result": {
"province": "浙江",
"city": "杭州",
"company": "中国移动",
"cardtype": "GSM"
}
}
assert result.status_code == 200
assert results['msg'] == "ok"
assert results['result']['province'] == "河南"
assert results['result']['city'] == "洛阳"
assert results['result']['company'] == "中国联通"
assert results['result']['cardtype'] is None
assert results['result']['areacode'] == "0379"

Pytest 指定目录(文件)执行

  • 不指定目录:pytest
    • 根目录下查找符合条件的文件及方法
  • 指定路径到目录 pytest .\test_request\
    • 指定目录下查找符合条件的文件及方法
    • 指定路径到文件pytest .\test_request\test_request.py
    • 指定文件内查找符合条件的方法

Pytest 运行参数详解

  • pytest -m : 执行特定的测试用例(标签) 执行命令 pytest -m test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# pytest.ini 文件中配置
# # 定义标签
# markers =
# p0: 高优先级
# test: 测试环境

@pytest.mark.test
def test_login_mark():
print("测试环境登录...")


# 高级用法, 给类打标签,执行类下的所有脚本
@pytest.mark.test
class TestOrderEnv:

def test_login_mark(self):
print("测试环境登录...")

def test_search(self):
print("测试环境查询...")




  • pytest -k : 执行用例包含 关键字 的用例

  • 先匹配文件名中的关键字,如果没有再匹配测试方法名称中的 关键字

  • 执行命令 pytest -k phone_number

1
2
3
4
5
6
def test_phone_number_with_get():
url = "https://api.binstd.com/shouji/query"
headers = {
'Content-Type': 'application/json;charset=UTF-8',
}
........
  • pytest -q : 说明 简化控制台输出
1
2
3
(env-pytest) D:\Business\AutoTest\ApiPytest>pytest -q test_scripts/test_mobile.py
.. [100%]
2 passed in 0.70s
  • pytest -v : 输出用例更加详细的执行信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(env-pytest) D:\Business\AutoTest\ApiPytest>pytest -v test_scripts/test_mobile.py
=================================================================================================== test session starts ====================================================================================================
platform win32 -- Python 3.10.14, pytest-8.3.2, pluggy-1.5.0 -- C:\Miniconda3\envs\env-pytest\python.exe
cachedir: .pytest_cache
rootdir: D:\Business\AutoTest\ApiPytest
configfile: pytest.ini
plugins: ordering-0.6
collected 2 items

test_scripts/test_mobile.py::test_phone_number_with_get PASSED [ 50%]
test_scripts/test_mobile.py::test_phone_number_with_post PASSED [100%]

==================================================================================================== 2 passed in 0.69s =====================================================================================================

  • pytest -s : 输出用例中的调试信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(env-pytest) D:\Business\AutoTest\ApiPytest>pytest -s test_scripts/test_mobile.py
=================================================================================================== test session starts ====================================================================================================
platform win32 -- Python 3.10.14, pytest-8.3.2, pluggy-1.5.0
rootdir: D:\Business\AutoTest\ApiPytest
configfile: pytest.ini
plugins: ordering-0.6
collected 2 items

test_scripts\test_mobile.py 测试手机号归属地 GET 请求
{'status': 0, 'msg': 'ok', 'result': {'shouji': '16638912000', 'province': '河南', 'city': '洛阳', 'company': '中国联通', 'cardtype': None, 'areacode': '0379'}}
.测试手机号归属地 POST 请求
{'status': 0, 'msg': 'ok', 'result': {'shouji': '16638912001', 'province': '河南', 'city': '洛阳', 'company': '中国联通', 'cardtype': None, 'areacode': '0379'}}
.

==================================================================================================== 2 passed in 0.69s =====================================================================================================
  • pytest.ini 配置指定运行参数
1
2
# 配置默认运行参数
addopts : -sv

Pytest 配置参数(ini 文件)

  • 创建 pytest.ini 文件
1
2
3
4
5
6
7
8
9
10
11
12
[pytest]
# 配置脚本执行路径
# testpaths=./test_request/test_request.py
testpaths=./test_scripts

# 定义标签
markers =
p0: 高优先级
test: 测试环境

# 配置默认运行参数
addopts : -sv

Pytest 的 Setup 和 Teardown

  • 模块级: setup_moduleteardowh_module 开始于模块(每个文件)始末,生效一次
1
2
3
4
5
6
7
def setup_module():
print("测试模块级前置条件...")


def teardown_module():
print("测试模块级后置条件...")

  • 函数级: setup_functionteardowh_function 对不在类中的每条函数都生效
1
2
3
4
5
6
def setup_function():
print("测试函数级前置条件...")


def teardown_function():
print("测试函数级后置条件...")
  • 类级: setup_classteardowh_class 只在类中前后生效一次,在类中
1
2
3
4
5
6
7
8
9
10
11
12
13
class TestMobile:
def setup_class(self):
print("测试类级前置条件...")

def teardown_class(self):
print("测试类级后置条件...")

def test_phone_number_with_get(self):
print("测试手机号归属地 GET 请求")

def test_phone_number_with_post(self):
print("测试手机号归属地 POST 请求")

  • 方法级: setup_methodteardowh_method 开始于方法始末,在类中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TestMobile:
def setup_method(self):
print("测试方法级前置条件...")

def teardown_method(self):
print("测试方法级后置条件...")

def test_phone_number_with_get(self):
print("测试手机号归属地 GET 请求")

def test_phone_number_with_post(self):
print("测试手机号归属地 POST 请求")


Pytest 的 Skip 和 Skipif

  • 标签跳过 @pytest.mark.skip
1
2
3
4
@pytest.mark.skip
def test_phone_number_with_get():
print("测试手机号归属地 GET 请求")

  • 条件跳过 @pytest.mark.skipif
1
2
3
4
5
6
moblile = "1300000111"

@pytest.mark.skipif('len(moblile) != 11')
def test_phone_number_with_get():
print("测试手机号归属地 GET 请求")

Pytest 参数依赖

不能使用 def __init__(self): 定义类属性

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
class TestUser:
UserToken = None
UserName = None

def test_login(self):
""" 登录 """
login_data = {
"username": "system",
"password": "system123"
}
token = str(uuid.uuid4()).replace('-', '')

TestUser.UserToken = token
TestUser.UserName = login_data.get('username')

def test_get_user_info(self):
"""
获取用户信息
"""
headers = {
'Content-Type': 'application/json;charset=UTF-8',
'X-Subject-Token': TestUser.UserToken,
'X-User-Name': TestUser.UserName
}
print(headers)
assert headers["X-Subject-Token"] == TestUser.UserToken
assert headers["X-User-Name"] == "sys"

Pytest 进阶用法

Pytest 进阶 fixture

fixture 是测试前后进行预备、清理工作的代码处理机制

相对于 Setup 和 Teardown 的优势:

  • 命名比较灵活,局限性比较小
  • conftest.py 文件配置可以实现数据共享,不需要 import 就可以自动找到一些配置
  1. fixture 夹具 @pytest.fixture 作用范围:session > module > class > function

(scope=’function’) 每一个函数或方法都会调用

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
# 默认 scope 是 function  即: @pytest.fixture() == @pytest.fixture(scope='function')
@pytest.fixture()
def fixture_func():
print("测试 fixture 前置条件...")

def test_phone_number_with_get(fixture_func):
print("测试手机号归属地 GET 请求")


# autouse = True 所有函数都会使用该前置条件
@pytest.fixture(autouse=True)
def fixture_func():
print("测试 fixture 前置条件...")

def test_phone_number_with_get():
print("测试手机号归属地 GET 请求")

def test_phone_number_with_post():
print("测试手机号归属地 POST 请求")


# 可以使用多个 fixture
@pytest.fixture(scope='function')
def fixture_function1():
print("测试 fixture 的 function1 ...")


@pytest.fixture(scope='function')
def fixture_function2():
print("测试 fixture 的 function2 ...")

def test_phone_number_with_get(fixture_function1, fixture_function2):
print("测试手机号归属地 GET 请求")

(scope=’class’) 每一个类调用一次;如果 类里边的方法 都加了 fixture, 只会在第一方法执行一次

1
2
3
4
5
6
7
8
9
10
11
12
13
@pytest.fixture(scope='class', autouse=True)
def fixture_func():
print("测试 fixture 前置条件...")



class TestClassFixture:

def test_phone_number_with_get(self):
print("测试手机号归属地 GET 请求")

def test_phone_number_with_post(self):
print("测试手机号归属地 POST 请求")

(scope=’module’) 每一个 .py 文件调用一次

1
2
3
4
5
6
7
8
9
10
11
@pytest.fixture(scope='module', autouse=True)
def fixture_func():
print("测试 fixture 前置条件...")


class TestClassFixture:
def test_phone_number_with_post(self):
print("测试手机号归属地 POST 请求")

def test_phone_number_with_get(self):
print("测试手机号归属地 GET 请求")

(scope=’session’) 是多个文件调用一次, 需要创建 conftest.py 文件

1
2
3
4
5
import pytest

@pytest.fixture(scope='session', autouse=True)
def test_session():
print("测试 fixture 的 session ...")
  1. conftest.py 文件功能

    • conftest.py 为固定写法,不可修改文件名字
    • 使用 conftest.py 文件方法不需要导入
    • 函数作用于当前文件夹及下属文件夹
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
# -*- coding: utf-8 -*-

"""
-----------------------
# Author: Always
# Desc:
# DateTime: 2024/8/24 23:09
-----------------------
"""
import pytest


@pytest.fixture(scope='session', autouse=True)
def fixture_session():
print("测试 fixture 的 session ...")


@pytest.fixture(scope='function')
def fixture_function():
print("测试 fixture 的 function ...")


@pytest.fixture(scope='module', autouse=True)
def fixture_module():
print("测试 fixture 的 module ...")


@pytest.fixture(scope='class', autouse=True)
def fixture_class():
print("测试 fixture 的 class ...")

  1. conftest.py 文件定义及使用
1
2
3
4
5
6
7
8
9
10
11
# 用户登录 获取 token
@pytest.fixture(scope='function')
def get_user_token():
""" 登录 """
login_data = {
"username": "system",
"password": "system123"
}
token = str(uuid.uuid4()).replace('-', '')

return token
1
2
3
4
5
6
7
8
9
10
11
# 使用:根据 token 获取用户信息
def test_get_user_info(get_user_token):
"""
获取用户信息
"""
headers = {
'Content-Type': 'application/json;charset=UTF-8',
'X-Subject-Token': get_user_token,
}
print(headers)
assert headers["X-Subject-Token"] == get_user_token
  1. yield 后置处理
1
2
3
4
5
6
7
@pytest.fixture(autouse=True)
def func():
print("我是前置步骤")
# yield 可迭代的对象, 前置结束后,执行业务测试代码,然后执行后置; yield 可以返回值
yield "返回值"
print("我是后置步骤")

  1. 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
30
31
32
33
34
35
36
37
38
39
40
@pytest.fixture(scope='session')
def fixture_session():
print("测试 fixture 的 session ...")


@pytest.fixture(scope='function')
def fixture_function():
print("测试 fixture 的 function ...")


@pytest.fixture(scope='module')
def fixture_module():
print("测试 fixture 的 module ...")


@pytest.fixture(scope='class')
def fixture_class():
print("测试 fixture 的 class ...")


class TestClassFixture:

def test_phone_number_with_post(self, fixture_class, fixture_session, fixture_module, fixture_function):
print("测试手机号归属地 POST 请求")


# 执行结果
# ============================= test session starts =============================
# collecting ... collected 1 item
#
# test_fixture_order.py::TestClassFixture::test_phone_number_with_post
# 测试 fixture 的 session ...
# 测试 fixture 的 module ...
# 测试 fixture 的 class ...
# 测试 fixture 的 function ...
# 测试手机号归属地 POST 请求
# PASSED
#
# ============================== 1 passed in 0.03s ==============================

  1. fixture 的调用方法

    • 可以接收返回值:@pytest.fixture(scope=’function’)
    • 无法接收返回值:@pytest.mark.usefixtures(“func”)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@pytest.fixture(scope='function')
def use_fixtures():
print("测试 fixture 的 use_fixtures ...")


@pytest.fixture(scope='function')
def use_fixtures1():
print("测试 fixture 的 use_fixtures1 ...")


@pytest.mark.usefixtures("use_fixtures")
@pytest.mark.usefixtures("use_fixtures1")
# 或 @pytest.mark.usefixtures("use_fixtures", "use_fixtures1")
def test_phone_number_with_get():
print("测试手机号归属地 GET 请求")
  1. fixture 的 params 和 ids:@pytest.fixture(params = [‘参数1’, ‘参数2’],ids = [‘用例1’, ‘用例2’])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@pytest.fixture(params=["data1", "data2"], ids=["test_data1", "test_data2"])
def params_fixtures(request):
print("测试 fixture 的 params ...")
return request.param


def test_params(params_fixtures):
print(params_fixtures)

# ============================= test session starts =============================
# collecting ... collected 2 items
#
# test_fixture_params.py::test_params[test_data1] 测试 fixture 的 params ...
# data1
# PASSED
# test_fixture_params.py::test_params[test_data2] 测试 fixture 的 params ...
# data2
# PASSED
#
# ============================== 2 passed in 0.03s ==============================

Pytest 数据驱动(parametrize)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 单次循环
# 运行时,将数组里的值分别复制给变量,每赋值一次,运行一次
@pytest.mark.parametrize("name", ["那可", "黄泉", "卡夫卡"])
def test_params(name):
print(name)

# 多次循环
@pytest.mark.parametrize("a, b", [("c,d"), ("e", "f")])
def test_params(a, b):
print(a, b)

# 参数值为字典
@pytest.mark.parametrize("info", [{'name': "那可", 'other': "纳科纳克"}, {'name': "黄泉", 'other': "一场空白"}])
def test_params3(info):
print(info.get('name'))

Pytest 数据驱动(Yaml)

安装 conda install pyyamlpip install pyyaml

语法

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
# 字典
# 'phone': {'province': '浙江', 'city': '杭州', 'company': '中国联通'}
phone:
province: "浙江"
city: "杭州"
company: "中国联通"

# 列表
# 'company': ['中国联通', '中国移动', '中国电信']
company:
- '中国联通'
- '中国移动'
- '中国电信'

# 组合
# 'phone_list': [{'province': '浙江', 'city': '杭州', 'company': '中国联通'}, {'province': '河南', 'city': '洛阳', 'company': '中国联通'}]
phone_list:
- province: "浙江"
city: "杭州"
company: "中国联通"
- province: "河南"
city: "洛阳"
company: "中国联通"


# 列表嵌套
# 'phone_lists': [['中国联通', '中国移动'], ['中国电信']]
phone_lists:
- - '中国联通'
- '中国移动'
- - '中国电信'


  1. Yaml 文件解析
1
2
3
4
5
6
import yaml

with open('../config/data.yaml', 'rb', encoding="utf8") as config:
data = yaml.safe_load(config)

print(data)
  1. Yaml 在线格式校验:
  2. Yaml + parametrize
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Yaml 文件
test_login:
- name: login_case1
request:
url: 'http://sellshop.5istudy.online/sell/user/login'
method: 'POST'
headers:
Content-Type: 'application/json;charset=UTF-8'
payload:
username: 'system'
password: 'system123'

mobile_belong: { 'shouji': '16638912001', 'appkey': '2528a4a09cea4224' }

mobile_belong_post:
- - shouji: '16638912001'
- appkey: '2528a4a09cea4224'

mobile_belong_get:
- [ '16638912001', '2528a4a09cea4224' ]
- [ '16638912002', '2528a4a09cea4224' ]
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 pytest

from base.parse_yaml import get_config

@pytest.mark.parametrize("login_data", get_config['test_login'])
def test_params(login_data):
print(login_data)
print(login_data['name'])
print(login_data['request'])
print(login_data['request']['payload'])


def test_mobile_with():
print("测试手机号归属地 GET 请求")
payload = get_config['mobile_belong']


@pytest.mark.parametrize('mobile, appkey', get_config['mobile_belong_post'])
def test_mobile_with_post(mobile, appkey):
print(mobile, appkey)


@pytest.mark.parametrize('mobile, appkey', get_config['mobile_belong_get'])
def test_mobile_with_get(mobile, appkey):
print(mobile, appkey)
  1. Yaml 中使用自定义函数

    Yaml 文件

1
2
3
4
5
6
7
8
user_info:
username: 杭州-${random_name()}-测试
age: ${age()}
mobile: ${mobile()}
province: "浙江"
city: "杭州"
company: "中国联通"

Yaml 函数逻辑文件func_yaml.py

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
# faker 生成模拟数据
import random

from faker import Faker

fake = Faker(locale="zh-CN")

# print(fake.name()) # 生成随机姓名
# print(fake.address()) # 生成随机地址
# print(fake.text()) # 生成随机文本
# print(fake.email()) # 生成随机邮箱
# print(fake.job()) # 生成随机职业
# print(fake.company()) # 生成随机公司名
# print(fake.phone_number()) # 生成随机电话号码

def func_yaml(data):
if isinstance(data, dict):
for key, value in data.items():
value = str(value)
if '${' and '}' in value:
start = value.index('${')
end = value.index('}')
func_name = value[start + 2:end]
# eval 表达式,执行函数, 参数是一个字符串
data[key] = value[0:start] + str(eval(func_name)) + value[end + 1:len(value)]
return data

def random_name():
return fake.name()


def age():
return random.randint(10, 100)


def mobile():
return fake.phone_number()

测试脚本中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import pytest
from base.parse_yaml import get_config
from base.func_yaml import func_yaml


def test_user_info():
user_data = get_config['user_info']
user_info = func_yaml(user_data)
print(user_info)


@pytest.mark.parametrize("data", get_config['user_info'])
def test_user_info_params(data):
user_info = func_yaml(data)
print(user_info)


Pytest 失败用例重跑

安装 conda install pytest-rerunfailures

1
2
3
4
5
6
7

--reruns n (重新运行次数)

--reruns-delay m (等待运行秒数)

# pytest.ini 配置默认运行参数
addopts : -sv --reruns 1 --reruns-delay 5 --alluredir ./report

Pytest 自动化框架

代码分层优化

INI 配置文件

安装 conda install configparserpip install configparser

1
2
3
4
5
6
# INI 配置文件
[HOST]
host = http://api.binstd.com



1
2
3
4
5
6
7
8
9
10
11
# 解析 ini 配置文件
def parser_ini():
ini_config = configparser.ConfigParser()
ini_config.read(ini_path, encoding="utf8")
print(ini_config['HOST']['host'])
print(ini_config['HOST'])

return ini_config


get_ini = parser_ini()

文件解析封装

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
# -*- coding: utf-8 -*-

"""
-----------------------
# Author: Always
# Desc:
# DateTime: 2024/8/25 18:12
-----------------------
"""
import os
import yaml
import configparser

# root_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
real_path = os.path.realpath(__file__)
root_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ini_path = os.path.join(root_path, 'config', 'setting.ini')
data_path = os.path.join(root_path, 'data', 'data.yaml')


class ParserFiles:
def __init__(self):
self.data_path = data_path
self.ini_path = ini_path

# 解析 yaml 配置文件
def parser_yaml(self):
with open(self.data_path, 'rb') as config:
data = yaml.safe_load(config)
# print(data.get('user_info'))
return data

# 解析 ini 配置文件
def parser_ini(self):
ini_config = configparser.ConfigParser()
ini_config.read(self.ini_path, encoding="utf8")
print(ini_config['HOST']['host'])
print(ini_config['HOST'])

return ini_config

... ...

def get_host(self):
return self.parser_ini()['HOST']['host']


ParserData = ParserFiles()

接口请求封装

  • http_request.py
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
67
68
69
70
71
72
73
74
75

# -*- coding: utf-8 -*-

"""
-----------------------
# Author: Always
# Desc:
# DateTime: 2024/8/25 19:06
-----------------------
"""
import json

import requests

from base.http.http_response import process_response
from base.log.log import logger
from base.parser_config import ParserData

base_url = ParserData.get_base_url()


class HttpRequest:
def __init__(self):
self.base_url = base_url
self.session = requests.Session()

def get(self, url, **kwargs):
return self.request(url, "GET", **kwargs)

def post(self, url, **kwargs):
return self.request(url, "POST", **kwargs)

def put(self, url, **kwargs):
return self.request(url, "PUT", **kwargs)

def delete(self, url, **kwargs):
return self.request(url, "DELETE", **kwargs)

def request(self, url, method, **kwargs):
request_url = self.base_url + url
self.request_log(request_url, method, **kwargs)

# if method == "GET":
# return self.session.get(request_url, **kwargs)
# if method == "POST":
# return self.session.post(request_url, **kwargs)
# if method == "PUT":
# return self.session.put(request_url, **kwargs)
# if method == "DELETE":
# return self.session.delete(request_url, **kwargs)

response = self.session.request(method, request_url, **kwargs)
return process_response(response)

def request_log(self, request_url, method, **kwargs):
request_data = dict(**kwargs).get("data")
request_json = dict(**kwargs).get("json")
request_params = dict(**kwargs).get("params")
headers = dict(**kwargs).get("headers")

logger.info("接口请求地址>>> {}".format(request_url))
logger.info("接口请求方法>>> {}".format(method))
if headers is not None:
logger.info("接口请求的 Headers 参数>>> \n{}".format(json.dumps(headers, indent=2)))

if request_data is not None:
logger.info("接口请求的 Data 参数>>> \n{}".format(json.dumps(request_data, indent=2)))

if request_json is not None:
logger.info("接口请求的 JSON 参数>>> \n{}".format(json.dumps(request_json, indent=2)))

if request_params is not None:
logger.info("接口请求的 Params 参数>>> \n{}".format(json.dumps(request_params, indent=2)))


  • http_response.py
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
# -*- coding: utf-8 -*-

"""
-----------------------
# Author: Always
# Desc:
# DateTime: 2024/8/25 22:34
-----------------------
"""
import json

from base.http.base_response import BaseResponse
from base.log.log import logger


def process_response(response):
status_code = response.status_code
if status_code == 200 or status_code == 201:
BaseResponse.success = True
BaseResponse.body = response.json()
logger.info("接口的响应内容>>> \n响应状态码:{} \n响应参数:\n{}".format(status_code, json.dumps(
response.json(), indent=2, ensure_ascii=False)))
else:
BaseResponse.success = False
BaseResponse.body = response.json()
logger.info("接口的响应状态码不符合预期>>> \n响应状态码:{} \n响应参数:\n{}".format(status_code, json.dumps(
response.json(), indent=2, ensure_ascii=False)))
return BaseResponse

  • base_response.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# -*- coding: utf-8 -*-

"""
-----------------------
# Author: Always
# Desc:
# DateTime: 2024/8/25 22:48
-----------------------
"""


class BaseResponse:
pass

  • api_data.py
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
# -*- coding: utf-8 -*-

"""
-----------------------
# Author: Always
# Desc:
# DateTime: 2024/8/25 19:29
-----------------------
"""

# Session = requests.Session()
from base.http.http_request import HttpRequest


class MobileApiData(HttpRequest):
def __init__(self):
super(MobileApiData, self).__init__()

def get_mobile_belong(self, **kwargs):
# kwargs 将参数 变成字典的形式
uri = "/shouji/query"
# TODO 请求方法优化
method = ""
return self.get(uri, **kwargs)

def post_mobile_belong(self, **kwargs):
# kwargs 将参数 变成字典的形式
uri = "/shouji/query"
return self.post(uri, **kwargs)


mobile_api_data = MobileApiData()

  • api.py
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
# -*- coding: utf-8 -*-

"""
-----------------------
# Author: Always
# Desc:
# DateTime: 2024/8/25 18:57
-----------------------
"""

from data.mobile_api_data import mobile_api_data


def mobile_request_get(payload):
headers = {
'Content-Type': 'application/json;charset=UTF-8',
}
result = mobile_api_data.get_mobile_belong(params=payload, headers=headers)
return result


def mobile_request_post(payload):
"""
测试 JSON 传参
:param payload:
:return:
"""

headers = {
'Content-Type': 'application/json;charset=UTF-8',
}
result = mobile_api_data.post_mobile_belong(json=payload, headers=headers)
return result

日志模块封装

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
# -*- coding: utf-8 -*-

"""
-----------------------
# Author: Always
# Desc:
# DateTime: 2024/8/25 21:11
-----------------------
"""
import os
import time
import logging

ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 定义日志文件路径
LOG_PATH = os.path.join(ROOT_PATH, "log")


class Logger:

def __init__(self):
# 定义日志位置和文件名
self.logname = os.path.join(LOG_PATH, "{}.log".format(time.strftime("%Y-%m-%d")))
# 定义一个日志容器
self.logger = logging.getLogger("log")
# 设置日志打印的级别
self.logger.setLevel(logging.DEBUG)
# 创建日志输入的格式
self.formater = logging.Formatter(
'[%(asctime)s][%(filename)s %(lineno)d][%(levelname)s]: %(message)s')
# 创建日志处理器,用来存放日志文件
self.filelogger = logging.FileHandler(self.logname, mode='a', encoding="UTF-8")
# 创建日志处理器,在控制台打印
self.console = logging.StreamHandler()
# 设置控制台打印日志界别
self.console.setLevel(logging.DEBUG)
# 文件存放日志级别
self.filelogger.setLevel(logging.DEBUG)
# 文件存放日志格式
self.filelogger.setFormatter(self.formater)
# 控制台打印日志格式
self.console.setFormatter(self.formater)
# 将日志输出渠道添加到日志收集器中
self.logger.addHandler(self.filelogger)
self.logger.addHandler(self.console)


logger = Logger().logger

if __name__ == '__main__':
logger.info("测试 INFO 日志")
logger.debug("测试 DEBUG 日志")
logger.warning("测试 WARNING 日志")
logger.error("测试 ERROR 日志")

Allure 测试报告

环境搭建

  1. 安装 conda install allure-pytestpip install allure-pytest
  2. Java 环境安装(jdk)
  3. Allure 命令行工具

运行机制

  1. pytest.ini 配置
1
addopts : -sv --alluredir ./report
  1. 报告查看

    • allure serve ./report

    • 生成 HTML 报告: allure generate report 文件名:allure-report

    • 生成 HTML 报告: allure generate report -o 文件名:allure_report

    • 打开 HTML 报告: allure open allure-report 或 allure open allure_report

  2. allure 使用方法

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

# -*- coding: utf-8 -*-

"""
-----------------------
# Author: Always
# Desc:
# DateTime: 2024/8/24 21:03
-----------------------
"""
import pytest
import allure

# 建立会话机制
from api.mobile_api import mobile_request_get, mobile_request_post
from base.log.log import logger


@allure.epic("数据进制项目 epic # 定义项目") # 定义项目
@allure.feature("手机号模块 feature # 定义模块名称") # 定义模块名称
class TestMobile:

@allure.story("测试手机号归属地 # 用例故事 story1 ")
@allure.title("手机号归属地 GET 请求 # 用例标题 title ")
@allure.testcase("https://www.baidu.com/", name="测试用例(功能用例)的连接地址 testcase")
@allure.issue("https://www.baidu.com/", name="缺陷提交地址 issue")
@allure.link("https://www.baidu.com/", name="链接地址 link")
@allure.description("测试手机号归属地 GET 请求, # 用例详细描述 description")
@allure.step("操作步骤 step")
@allure.severity(severity_level="blocker") # 用例优先级
def test_phone_number_with_get(self):
print("")
payload = {
'shouji': '16638902001',
'appkey': '2528a4a09cea4224',
}
results = mobile_request_get(payload)

assert results.success is True
assert results.body['msg'] == "ok"

@allure.story("测试手机号归属地 # 用例故事 story2 ")
@allure.title("手机号归属地 POST 请求 # 用例标题 title ")
@allure.testcase("https://www.baidu.com/", "测试用例(功能用例)的连接地址 testcase")
@allure.issue("https://www.baidu.com/", "缺陷提交地址 issue")
@allure.description("测试手机号归属地 POST 请求, # 用例详细描述 description")
@allure.step("操作步骤 step")
@allure.severity(severity_level="blocker") # 用例优先级
def test_phone_number_with_post(self, log_wrapper):
logger.info("开始测试登录 POST 请求")
payload = {
"username": "system",
"password": "system123"
}
print(payload)
results = mobile_request_post(payload)
assert results.success is True
assert results.body['msg'] == "ok"


if __name__ == '__main__':
pytest.main()
  1. allure 动态自定义报告
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
import allure

# 建立会话机制
from api.mobile_api import mobile_request_get, mobile_request_post
from base.log.log import logger
from base.parser_config import ParserData


@allure.epic("数据进制项目 epic # 定义项目") # 定义项目
@allure.feature("手机号模块 feature # 定义模块名称") # 定义模块名称
class TestMobile:

def test_phone_number_with_get(self):
mobile_belong_dynamic = ParserData.parser_yaml()['mobile_belong_dynamic']
# 动态自定义 Allure
allure.dynamic.story(mobile_belong_dynamic['story'])
allure.dynamic.title(mobile_belong_dynamic['title'])
allure.dynamic.description(mobile_belong_dynamic['description'])

payload = {
'shouji': mobile_belong_dynamic['params']['shouji'],
'appkey': mobile_belong_dynamic['params']['appkey'],
}
results = mobile_request_get(payload)

assert results.success is True
assert results.body['msg'] == "ok"

  1. allure 添加环境信息

report 目录下创建 environment.properties

1
2
3
4
5
# 字段无要求,随便写
author=Always
version=5.0
base_url=https://www.baidu.com/
env=pro