Skip to content

Api 速通

只整理常用的功能,可能会有一些遗漏,不过应该能够应付大多数日常开发场景。

约定规范

IMPORTANT

  • 默认的测试发现规则是 从项目根目录,查找 test_*.py*_test.py 文件,然后 Test 开头的类以及 test_ 开头的方法

  • conftest.py 是通用 fixture 声明的地方,该文件名是约定俗成的,pytest 会自动导入该文件中的 fixture

  • caplogpytest 提供的 fixture,用于获取日志信息

py
def test_case_01(caplog):
    return caplog

全局变量下的 pytest.mark.parametrize 变量名必须叫 pytestmark

py
pytestmark = pytest.mark.parametrize('exec, expected', [
    ('2*3', 6),
    ('3*3', 9)
])

Fixture

使用 @pytest.fixture 装饰的函数,被称为装置支持作用域、参数化、自动注入、测试前置和后置操作、复写以及参数化中个性配置

py
# 作用域和自动注入
@pytest.fixture(scope="session", autouse=True)
def fixture_name():
    pass

# 测试前置和后置操作
@pytest.fixture
def fixture_name():
    print('前置操作')
    yield
    print('后置操作')

# 参数化
@pytest.fixture(params=[1, 2, 3])
def fixture_name(request):
    return request.param

# 参数化中的个性配置
# 针对某一个参数使用 mark 进行标记,单个或者多个
@pytest.fixture(params=[1, 2, pytest.param(3, marks=pytest.mark.skip)])
def fixture_name(request):
    return request.param

usefixtures

默认情况下要使用哪些 fixture 实际上都会在当做参数传递到测试用例中。比如这样

py
@pytest.fixture
def data_fixture():
    print('test staring...')
    yield 'test data'
    print('test ending...')

def test_01(data_fixture):
    assert data_fixture == 'test data'

实际上有些时候,你可能压根用不到 yield 的值,比如他只负责在测试之前创建一个文件,结束之后就要删除文件了。

py
@pytest.fixture
def data_fixture():
    print('test staring...')
    os.mkdir(os.path.abspath('test_dir'))
    time.sleep(5)
    yield 
    os.rmdir(os.path.abspath('test_dir'))
    print('test ending...')

# 此时其实你完全不用关心 yield 的值
# 就可以使用 usefixtures
def test_01(data_fixture):
    assert 1 == 1

# 可以接收任意个 usefixtures("data_fixture", "data_fixture2")
@pytest.mark.usefixtures("data_fixture")
def test_02():
    assert 1 == 1

至于 usefixture 的执行次数,完全依赖于你使用的 fixturescope

Mark

准确的说 usefixture 也是在 mark 的范畴内。此外还有一些常用的 mark

支持跳过用例、标记失败、标记慢用例、标记 xfail 、过滤警告信息。

py
@pytest.mark.skip(reason='老版本的测试用例,暂时跳过')
def test_case_02():
    assert 1 == 1

@pytest.mark.skipif(condition=True , reason='满足条件就跳过这个用例')
def test_case_03():
    assert 1 in [1]

@pytest.mark.xfail(reason='标记失败')
def test_case_04():
    assert 1 == 2

# 包含 api v1 的警告信息不会出现
@pytest.mark.filterwarnings('ignore: api v1')
def test_case_05():
    warnings.warn(UserWarning('api v1, 这是老的 api 尽快更新到 api v2'))
    return 1

# 1. 参数化接收输入和预期结果
@pytest.mark.parametrize('test_input, expected', [(1, 2), (2, 3)])
def test_case_06(test_input, expected):
    assert test_input + 1 == expected

# 2. 同样可以使用 pytest.param 对参数化的某一项进行标记
@pytest.mark.parametrize('exec, expected', [
    ('2*3', 6),
    ('3*3', 9),
    pytest.param('4*3', 11, marks=pytest.mark.xfail)
])
def test_case_07(exec, expected):
    assert eval(exec) == expected

# 3. 通过 pytestmark 来指定参数化,变量名必须叫 pytestmark
pytestmark = pytest.mark.parametrize('exec, expected', [
    ('2*3', 6),
    ('3*3', 9),
    pytest.param('4*3', 11, marks=pytest.mark.xfail)
])

def test_case_07(exec, expected):
    assert eval(exec) == expected

日志管理

pytest 默认捕获 WARNING 级别的日志并且会记录模块、行号、日志级别和消息

py
import logging

log = logging.getLogger(__name__)

def get_api_data():
    log.warning("get api data warning...")
    return "api data"

def test_case_01():
    assert get_api_data() == "api dataxx"

当用例执行错误时,会显示如下信息。

src/tests/test_case_01.py:11: AssertionError
---------------------------------------------------------- Captured log call -----------------------------------------------------------
注意:信息在这里
WARNING  test_case_01:test_case_01.py:7 get api data warning...
======================================================= short test summary info ========================================================
FAILED src/tests/test_case_01.py::test_case_01 - AssertionError: assert 'api data' == 'api dataxx'
========================================================== 1 failed in 0.03s ===========================================================

在执行的时候,可以指定输出时间格式。当然这些也可以通过配置文件进行配置

bash
pytest --log-format="%(asctime)s %(levelname)s %(message)s" \
        --log-date-format="%Y-%m-%d %H:%M:%S"
src/tests/test_case_01.py:11: AssertionError
---------------------------------------------------------- Captured log call -----------------------------------------------------------
注意:增加了时间的显示
2025-09-15 22:20:37 WARNING get api data warning...
======================================================= short test summary info ========================================================
FAILED src/tests/test_case_01.py::test_case_01 - AssertionError: assert 'api data' == 'api dataxx'
========================================================== 1 failed in 0.03s ===========================================================

也可以通过 --show-capture=no 来关闭日志的输出。也可以通过 --log-disable= 指定测试用例名字禁止输出日志。

pytest 中提供了一个全局的 fixture 名为 caplog 它可以获取日志的信息,也可以临时的修改捕获日志的级别。

py
import logging

def get_api_data():
    log.debug('get api data debug...')
    log.warning("get api data warning...")
    log.error("get api data error...")
    return "api data"

# 此时执行的时候就可以捕获到三条日志
def test_case_01(caplog):
    caplog.set_level(logging.DEBUG)

可以根据日志的返回信息进行断言。虽然我也不知道怎么用,或许是记录到文件之后才能打印出来吧。

py
def test_case_01(caplog):
    # * caplog.messages        -> 日志消息列表
    # * caplog.text            -> 日志文本信息
    # * caplog.records         -> logging.LogRecord 格式的日志列表
    # * caplog.record_tuples   -> 包含 log level name 和 message 的元组列表
    # * caplog.clear()         -> 清空日志

最后通过 --log-file=xx.log 配置日志的生成,但是每次执行用例都会复写之前的 log 文件;此配置同样可以在配置文件中完成。

  • 通过 --log-file-mode=a 来追加日志。
  • 通过 --log-file-level=INFO 来指定日志级别。
  • 通过 --log-date-format=%Y-%m-%d %H:%M:%S 指定输出的时间格式。与 --log-file-date-format 一样的效果
  • 通过 --log-format=%(asctime)s %(levelname)s %(message)s 来指定输出的格式。与 --log-file-format 一样的效果
bash
pytest --log-file=log/test.log --log-file-mode=a

配置文件

配置文件可以是 pytest.ini.pytest.ini 或者是 pyproject.toml 其他的都不推荐。一般存储在项目的根目录中。

参考文档

ini
# pytest.ini or .pytest.ini
[pytest]
minversion = 6.0
addopts = -ra -q
testpaths =
    tests
    integration

# pyproject.toml
[tool.pytest.ini_options]
minversion = "6.0"
addopts = "-ra -q"
testpaths = [
    "tests",
    "integration",
]

日志配置

ini
[pytest]
# 指定日志格式
log_format = %(asctime)s %(levelname)s %(message)s
# 配置日志时间格式
log_date_format = %Y-%m-%d %H:%M:%S
# 指定日志文件
log_file = ./log/test.log
# 追加写入模式
log_file_mode = a
# 记录 log 级别
log_file_level = DEBUG
# 多行日志缩进 True | On 、 False | Off
log_auto_indent=True # 缩进

测试发现规则配置

ini
[pytest]
# 正则,结尾包含 Suite 的类作为测试集合。可以是多个,中间加空格
# 默认以 Test 开头的类作为测试集合。
python_classes = *Suite *Test

# 正则,指定哪些文件被视为测试模块。默认 `test_*.py` 和 `*_test.py`
# 可以是多个,也可以是换行
python_files = test_*.py check_*.py example_*.py
python_files =
    test_*.py
    check_*.py
    example_*.py

# 正则,执行哪些测试方法和函数被视为测试用例,可以是多个,中间加空格
# 默认以 test_ 开头的方法为测试用例。
python_functions = *_test

# 设置测试从哪个目录开始执行,默认是项目根目录
testpaths = testing_dir

其他常用配置项

ini
[pytest]
# 值会被添加到命令行中,比如 -v 就相当于 pytest -v
# 可以指定多个,中间加空格
addopts = -v --maxfail=2

# 默认目录是 .pytest_cache
cache_dir = .pytest_cache

# 运行测试时,控制台输出的样式,默认是 classic
# 支持 progress count times
console_output_style = classic

# 过滤警告信息,默认情况下所有的警告都会被显示在控制台中
# 可以使用此配置,过滤某些警告不显示
# 与 pytest.mark.filterwarnings 一样的效果
filterwarnings = 
    error
    ignore::DeprecationWarning

# 带有 @pytest.mark.xfail 即使成功,默认情况下也将失败
xfail_strict = True

# 建议配置 --strict-markers 使用,代表只能使用已知的标记
addops = --strict-markers
markers = 
    slow
    serial

命令行标识

实际上能够放在配置文件中的配置,也可以通过命令行去执行。只不过比较复杂,写起来很不方便。 参考文档

Allure Report 生成

Allure Report 是一个测试报告生成工具,可以生成漂亮的测试报告。支持各种测试框架。

生成报告需要两步,第一步是安装 allure-pytest 它会帮助你生成测试报告的内容,但是该内容都是 json | txt 可读性不高。

bash
pip install allure-pytest

第二步是 allure serve 它会帮助你生成测试报告的界面。需要在当前环境安装,它依赖于 java

当前以 macOS 为例,当然也支持其他平台。安装文档

bash
brew install allure

前期准备

仍然以最小的 todo 项目为例。如果需要 server 代码可以在这里获取 点击获取

假设你的文件结构如下:

src/
├── tests/
│   ├── conftest.py
│   ├── test_todo.py
│   └── api/
│       └── api_client.py
py
import pytest
from api.api_client import TodoAPIClient

@pytest.fixture(scope='session')
def api_client():
    """ api 请求"""
    client = TodoAPIClient()
    yield client
    client.session.close()

@pytest.fixture
def get_new_todo(api_client):
    res = api_client.create_todo({
        'title': "新建的 todo",
        'description': "新建的 todo 的描述"
    })
    yield res
    # 后置操作将创建的 todo 删除
    try:
        api_client.delete_todo_by_id(res.json()['id'])
    except Exception:
        pass
py
import pytest
import warnings

class TestTodo:
    def test_create_todo(self, api_client, get_new_todo):
        todo = get_new_todo.json()
        assert get_new_todo.status_code == 201

        query_todo = api_client.get_todo_by_id(todo['id'])
        assert query_todo.json()['id'] == todo['id']
        
    def test_delete_todo(self, api_client, get_new_todo):
        todo = get_new_todo.json()

        res = api_client.delete_todo_by_id(todo['id'])
        assert res.status_code == 200

    def test_update_todo(self, api_client, get_new_todo):
        todo = get_new_todo.json()
        api_client.update_todo_by_id(
            todo['id'],
            {
                'title': '更新 todo',
                'description': '更新 todo 描述'
            }
        )
        res = api_client.get_todo_by_id(todo['id'])
        assert res.json()['title'] == '更新 todo'
        assert res.json()['description'] == '更新 todo 描述'

    def test_complete_todo(self, api_client, get_new_todo):
        todo = get_new_todo.json()
        api_client.change_todo_is_done_by_id(todo['id'], False)
        res = api_client.get_todo_by_id(todo['id'])
        assert res.json()['completed'] is True

    def test_incomplete_todo(self, api_client, get_new_todo):
        todo = get_new_todo.json()
        api_client.change_todo_is_done_by_id(todo['id'], True)
        res = api_client.get_todo_by_id(todo['id'])
        assert res.json()['completed'] is False
    
    # 参数化用例
    @pytest.mark.parametrize('data, expected', [
        ({ 'description': '没有 title 的数据' }, { 'error': 'Title is required', 'status_code': 400 }),
        ({ 'title': '没有 description 的数据' }, { 'error': 'Description cannot be empty', 'status_code': 400 }),
    ])
    def test_create_todo_invalid(self, api_client, data, expected):
        res = api_client.create_todo(data)

        assert res.json()['error'] == expected['error']
        assert res.status_code == expected['status_code']

    # 异常用例 -> 跳过
    @pytest.mark.skip
    def test_todo_skip(self, api_client):
        res = api_client.get_todo_list()
        assert res.status_code == 200

    # 异常用例 -> 标记失败
    @pytest.mark.xfail
    def test_todo_xfail(self, api_client):
        res = api_client.get_todo_list()
        assert res.status_code == 200

    # 警告用例 -> 调用不存在的方法
    def test_todo_warning(self, api_client):
        # 没有这个方法在 allure 中会提示警告用例
        res = api_client.get_todos()
        assert res.status_code == 200

    # 错误用例 -> 断言错误
    def test_todo_error(self, api_client):
        res = api_client.get_todo_list()
        # 断言错误在 allure 中是错误用例
        assert res.status_code == 201
py
import requests

class TodoAPIClient:
    def __init__(self):
        self.base_url = 'http://localhost:8080'
        self.session = requests.Session()

    def get_todo_by_id(self, id):
        return requests.get(f"{self.base_url}/todos/{id}")

    def get_todo_list(self):
        return requests.get(f"{self.base_url}/todos")

    def create_todo(self, data):
        return requests.post(f"{self.base_url}/todos", json=data)

    def update_todo_by_id(self, id, data):
        return requests.put(f"{self.base_url}/todos/{id}", json=data)

    def delete_todo_by_id(self, id):
        return requests.delete(f"{self.base_url}/todos/{id}")

    def change_todo_is_done_by_id(self, id, is_done):
        api_path = f'/todos/{id}/complete' if not is_done else f'/todos/{id}/incomplete'
        return requests.patch(f"{self.base_url}{api_path}")

同样使用 pytest 指令运行脚本,只是需要增加一个 --alluredir 参数。后面指定一个目录,allure 会将测试报告生成到这个目录中。

bash
pytest --alluredir allure-results

执行后会在当前目录下生成 allure-results 目录,里面是 json | txt 格式的内容。然后使用以下指令,即可在本地启动一个服务,查看测试报告。

bash
allure serve allure-results

测试报告会根据当前测试用例的组织方式(目录)生成对应的信息,对于失败和警告的用例也可以查看具体原因。大致如下。

alt text

丰富测试报告内容

allure.title

@allure.title('') 可以使用在测试用例以及 fixture 上面。当使用后,会在套件中得到显示。同样支持运行时 api

py
import allure

@allure.title('')
def test_01():
    pass

def test_02():
    allure.dynamic.title('')
    pass

对于参数化的测试用例可以使用 format 的方式拿到参数。

py
@pytest.mark.parametrize('test_input, expected', [(1, 2), (2, 3)])
@allure.title('测试参数化 {test_input}, {expected}')
def test_case_params():
    pass

alt text

allure.description

@allure.description('') 为测试用例指定描述,支持文档字符串,同样支持运行时 api

py
import allure

@allure.description('')
def test_01():
    pass

def test_02():
    allure.dynamic.description('')
    pass

def test_03():
    """ 描述 """
    pass

alt text

allure.tag

@allure.tag('', '') 指定一系列的标签,有助于测试用例的分类。支持运行时 api

py
import allure

@allure.tag('', '')
def test_01():
    pass

def test_02():
    allure.dynamic.tag('', '')
    pass

alt text

allure.severity

@allure.severity('') 指定测试用例的优先级支持运行时 api

接收 trivial、minor、normal、critical、blocker。分别对应琐碎、轻微、正常、关键、阻断。默认是 normal

py
import allure
from allure_commons.types import Severity

@allure.severity(Severity.BLOCKER)
def test_01():
    pass

def test_02():
    allure.dynamic.severity(Severity.NORMAL)
    pass

alt text

allure.suite

套件分为三个 suite, sub_suite, parent_suite 分别对应自身、子集、父集。一般在类中使用。

分别也对应三个位置,suite 对应当前的文件;sub_suite 对应当前的类;parent_suite 则对应当前文件的父级目录。

py
import allure

@allure.parent_suite('包含 TODO、等接口测试')
@allure.suite('TODO 接口测试')
@allure.sub_suite('TODO 测试类')
class TestTodo:
    
    def test_01():
        pass

alt text

allure.link、issue、testcase

可以指定链接、问题、测试用例。支持运行时 api

假设你是开源项目或者你的测试用例使用在线托管,这样做可以让你的用例更加明确是在实现什么功能。以及问题和测试用例的对应关系。

py
import allure

@allure.link(url="测试系统网站链接", name="xx医疗系统")
@allure.issue(url="测试功能问题链接", name="issue-1")
@allure.testcase(url="功能测试用例链接", name="testcase-1")
def test_01():
    pass

def test_02():
    allure.dynamic.link('')
    allure.dynamic.issue('')
    allure.dynamic.testcase('')
    pass

获取你直接使用 link 传入一个 type,即可实现 issue | testcase 的效果。

py
import allure
from allure_commons.types import LinkType

@allure.link(url="测试系统网站链接", name="Website", link_type=LinkType.LINK)
@allure.link(url="测试功能问题链接", name="Issue", link_type=LinkType.ISSUE)
@allure.link(url="功能测试用例链接", name="Test Case", link_type=LinkType.TEST_CASE)
def test_01():
    pass

alt text

allure.label

label 用于指定标签,支持运行时 api这是一个语法糖,支持创建很多种的标签

py
import allure
from allure_commons.types import LabelType, Severity
# LabelType 的定义
# class LabelType(str):
#     EPIC = 'epic'
#     FEATURE = 'feature'
#     STORY = 'story'
#     PARENT_SUITE = 'parentSuite'
#     SUITE = 'suite'
#     SUB_SUITE = 'subSuite'
#     SEVERITY = 'severity'
#     THREAD = 'thread'
#     HOST = 'host'
#     TAG = 'tag'
#     ID = 'as_id'
#     FRAMEWORK = 'framework'
#     LANGUAGE = 'language'
#     MANUAL = 'ALLURE_MANUAL'

# 比如我定义一个优先级
@allure.label(LabelType.SEVERITY, Severity.BLOCKER)
def test_01():
    pass

# 比如我定义一个 tag
@allure.label(LabelType.TAG, 'test')
def test_02():
    pass

配置项

allure 的配置同样支持 pytest.inipyproject.toml

ini
[pytest]
addopts = --alluredir results_dir
          --clean-alluredir
ini
[tool.pytest.ini_options]
addopts = [
    "--alluredir", "allure-results",
    "--clean-alluredir"
]
参数说明示例
--alluredir指定生成测试报告的目录
没有则新增
pytest --alluredir results_dir
--clean-alluredir新的结果生成之前删除旧的pytest --clean-alluredir
--allure-severities指定优先级pytest --allure-severities normal,minor
--allure-label指定标签pytest --allure-labels key=value,...

labelkey 支持的参数来源于 LabelType

项目实战

被测项目 这是一个基于 AI 开发的图书管理系统,后端使用 Python + Flask,前端使用 Vue + ElementPlus

其中后端使用 docker 启动,前端则是本地启动。具体可以查看该项目的 README.md 文件。

自动化接口测试

自动化接口测试使用 pytest 框架,使用 allure 作为测试报告生成器。地址

内涵接口、UI、以及一些测试脚本。在 src/main.py 下通过 argparse 模块对参数进行解析。实现对不同环境,不同参数的测试。