ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

pytest接口测试框架进阶:fixture、数据驱动与工程化落地

pytest接口测试框架进阶:fixture、数据驱动与工程化落地 1. 从入门到实战pytest接口测试的思维升级上一篇我们聊了pytest做接口测试的基本玩法能写用例、能跑断言、能输出报告这时候大部分人会觉得“我已经会了”。但真把放到实际项目中发下根本不是那么回事用例堆在一起、接口关联处理不好、登录态到处失效、上百个用例跑起来又慢又乱这才是真实情况。这篇续集就是把那些基础文档里不会细讲的东西一个个拆开聊明白。如果你现在属于这种状态——pytest基础语法都懂requests也能写一到搭框架就不知道从哪下手那这篇就是给你准备的。我不会再重复讲“pytest怎么安装、断言怎么写、mark怎么标记”这些基础课我们直接进入工程化落地阶段聊逻辑、聊取舍、聊踩坑让这套框架真正能拿去干活。1.1 先看一个真实的项目结构接口测试做大了以后最怕的是代码结构乱成一锅粥。我见过太多团队所有用例全部塞在一两个.py文件里fixture全部堆在同一个conftest数据文件跟源码混在一起跑起来靠运气维护起来想骂人。一个比较成熟的pytest接口测试工程目录结构通常是这样的api_test/ ├── config/ │ ├── __init__.py │ ├── settings.py # 全局配置环境地址、超时、重试次数 │ └── environments.yaml # 多环境配置 ├── common/ │ ├── __init__.py │ ├── http_client.py # requests.Session封装 │ ├── assert_utils.py # 断言工具 │ ├── log_utils.py # 日志封装 │ └── yaml_utils.py # yaml读取 ├── testcases/ │ ├── __init__.py │ ├── conftest.py # 用例层fixture │ ├── test_user_module/ │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_login.py │ │ └── test_profile.py │ └── test_order_module/ │ ├── __init__.py │ ├── conftest.py │ └── test_pay.py ├── data/ │ ├── __init__.py │ ├── login_data.yaml │ └── order_data.json ├── reports/ │ ├── allure-results/ │ └── allure-html/ ├── pytest.ini ├── requirements.txt └── README.md很多初学者会问为什么要分这么多层直接一个test_case.py不是也能跑吗能跑但你没考虑过一个问题接口用例数量从20条涨到500条的时候你怎么办一个文件5000行代码来回翻定位一条用例都费劲。分层不仅仅是代码洁癖是直接决定这套框架能不能在真实项目中活过三个迭代周期的关键。配置文件单独拿出来environment.yaml管环境差异settings.py管全局静态配置。common层放所有跟业务无关的通用能力比如HTTP客户端、断言工具。testcases按业务模块分子目录每个子目录有自己的conftest.py管理模块内的fixture根目录的conftest.py管理全局fixture。data目录跟源码分离改测试数据不用动代码。这样分完之后新增一条用例只需要做一件事在对应的业务模块目录下新建一个test_xxx.py文件根本不需要去碰公共代码。团队协作起来效率高很多也不会互相冲突。1.2 入口文件与配置别再裸奔pytest.ini是整个框架的“控制面板”我跟很多人聊天时发现他们压根不用pytest.ini全靠命令行一个个加参数。你想想十几条参数每次敲一遍早晚有一天会漏掉关键的而且不同同事敲的参数还不一样跑出来的结果都不一样排查问题的时候特别难受。一份比较完整的pytest.ini长这样[pytest] testpaths testcases python_files test_*.py test_*.yaml python_classes Test* python_functions test_* addopts -v --tbshort --strict-markers --alluredirreports/allure-results --maxfail5 markers smoke: 冒烟测试用例 regression: 回归测试用例 slow: 慢用例默认跳过 p0: P0级别用例 log_cli true log_cli_level INFO log_cli_format %(asctime)s [%(levelname)s] %(message)s log_cli_date_format %Y-%m-%d %H:%M:%S说一下几个容易被忽略的点。testpaths限定用例搜索目录这是你跑pytest时不需要指定路径的前提。--strict-markers是很多老手都会开的选项它的作用是如果用例上标记了一个没在markers中声明的标签直接报错。这个功能可以强制团队用统一的标记不会出现“张三写个danger、李四写个risk两个人还都以为对方写错了”的乌龙。--tbshort是错误回溯信息的显示格式。默认的long模式遇到几百条case的时候海量堆栈能把人淹没改成short只显示关键错误信息排查效率高一大截。environments.yaml是环境管理的首选方式dev: base_url: http://192.168.1.100:8080 db_host: 192.168.1.100 db_port: 3306 account: admin: username: dev_admin password: Dev2024# test: base_url: https://test.api.shop.example.com db_host: 10.10.10.20 db_port: 3306 account: admin: username: test_admin password: Test2024#用yaml格式而不是json格式主要是yaml支持注释、可读性好而且层次结构看起来更直观。环境配置文件尽量不放进测试报告目录避免敏感信息泄露。很多公司还会把生产环境的地址配进去但通常都会用环境变量或者加密方式处理这个后面聊环境切换时再展开。2. fixture不是装饰器而是你的依赖注入系统很多人理解fixture就是“setup和teardown的替代品”这是天大的误解。pytest的fixture本质是一个依赖注入框架你应该用它来管理测试对象的生产和销毁而不是简单地“跑前干点事”。2.1 fixture作用域怎么选直接决定用例能跑多快fixture的scope参数支持function、class、module、package、session这五种。我见过不少项目的fixture全都不写scope默认按function执行结果是每个用例都重新初始化一次性能被白白浪费。实际项目里选作用域有一套相对成熟的经验session级别登录token、环境配置、数据库连接、全局唯一的测试数据生成。这些只需要做一次整个测试过程共享。module级别模块内的公共数据准备比如创建一批订单、初始化一个测试店铺。同模块多个用例共用的数据放这里。function级别几乎每个用例都不同的操作比如清空某个消息通知、获取当前时间戳生成唯一订单号。class级别用得少如果你的用例确实按照class组织并且class里有公共的前置操作可以考虑。最典型的应用是登录态管理。接口测试90%以上的接口都需要token如果你让每个用例都登录一次那不是在测接口是在给登录接口做压力测试。正确做法是把登录做成session级别的fixtureimport pytest import requests pytest.fixture(scopesession) def login_token(env_config): 登录一次获取全局token并缓存 url f{env_config[base_url]}/api/auth/login payload { username: env_config[account][admin][username], password: env_config[account][admin][password] } resp requests.post(url, jsonpayload, timeout10) resp.raise_for_status() token resp.json()[data][token] yield token # session结束时可以做清理比如调用登出接口让服务端token失效 logout_url f{env_config[base_url]}/api/auth/logout requests.post(logout_url, headers{Authorization: fBearer {token}}, timeout5)登录这个fixture只执行一次后续所有用例共享同一个token。session结束才登出不会因为用例执行完就销毁导致后续用例拿不到token。这就是session作用域的典型用法。2.2 conftest分层与工厂模式让数据准备不打架conftest.py是fixture的“名字空间”。根目录的conftest.py定义的fixture全局可见子目录的conftest.py只对当前目录及子目录生效。这个特性用来做模块级数据管理非常顺手。举一个真实场景在订单测试模块里下单、支付、退款这三组用例都需要“一个已存在的订单”。但每组用例对订单状态的要求不一样比如“下单用例”关心创建订单成功、“支付用例”关心订单能正常支付、“退款用例”关心订单已支付成功。如果你在模块级conftest里只准备一个固定订单那么三组用例共用一个订单号用例之间会产生强依赖前面用例改了订单状态后面用例就直接挂掉。解决思路是使用工厂模式fixture。fixture不直接返回数据而是返回一个函数调用函数时再生成全新的数据import pytest import time import random pytest.fixture(scopemodule) def create_order(http_client, env_config): 工厂模式fixture每次调用生成一个新的订单 created_orders [] def _create_order(item_id1001, count1, remark测试订单): payload { item_id: item_id, count: count, remark: f{remark}_{int(time.time())}_{random.randint(1000, 9999)} } resp http_client.post(/api/order/create, jsonpayload) assert resp.status_code 200 order_id resp.json()[data][order_id] created_orders.append(order_id) return {order_id: order_id, item_id: item_id, count: count} yield _create_order # 模块结束批量清理这module内创建的所有订单 for oid in created_orders: http_client.post(/api/order/cancel, json{order_id: oid})这样每组用例调用create_order()时都会得到全新的订单彼此互不干扰测试用例之间的独立性大大提高。模块结束时自动清理所有创建的订单也不会给测试环境留下垃圾数据。我见过很多项目根本不做数据清理跑完一遍测试环境里堆满了测试订单下一次再跑时就出现“库存不足”、“唯一索引冲突”之类的诡异问题。数据清理这件事几乎决定了你的接口测试能不能稳定重复执行。3. 参数化与数据驱动把用例和数据彻底分开pytest.mark.parametrize是pytest最强大的特性之一但多数人只用了它最基础的用法直接在装饰器里写几组数据。真实项目里数据量的规模很快会超过你“在代码里手写”的承受范围。3.1 外部数据文件 parametrize 的完整链路我之前做过一个订单模块的接口测试光“创建订单”这一个接口就有十几种异常场景商品不存在、库存不足、数量为0、数量为负数、价格不对、用户无权限、请求参数缺少必填项、参数类型错误……加起来好几十条用例。如果全部用代码硬编码代码量和用例逻辑混在一起后面别人接手想改数据都无处下手。这时候就需要把数据抽离到外部文件。数据文件我通常用yaml结构清晰且支持注释。创建订单这个接口的测试数据大概是这样的test_create_order: - id: create_order_success description: 正常下单应成功 payload: item_id: 1001 count: 1 remark: e2e_订单_正常 expect: code: 0 status: 200 - id: create_order_invalid_item description: 商品不存在应报错 payload: item_id: 999999 count: 1 remark: expect: code: 40001 status: 200 - id: create_order_zero_count description: 数量为0应报错 payload: item_id: 1001 count: 0 remark: expect: code: 40002 status: 200对应地用例文件就非常干净import pytest from common.yaml_utils import load_yaml test_data load_yaml(data/order_data.yaml) pytest.mark.parametrize(case, test_data[test_create_order], idslambda c: c[id]) def test_create_order(http_client, case): 创建订单接口用例 resp http_client.post(/api/order/create, jsoncase[payload]) assert resp.status_code case[expect][status] resp_data resp.json() assert resp_data[code] case[expect][code]ids参数会让你在执行结果里直接看到每条用例的业务含义比如create_order_invalid_item而不是看到case0、case1这样的无意义编号。用例数据和执行逻辑完全分离之后运营同学或者测试新手只要跟着数据模板填内容就能新增一条用例不需要动任何代码逻辑这个可维护性比硬编码高一个量级。3.2 indirect用法让参数先过一道fixtureparametrize还藏着一个高级用法叫indirect。它允许你把参数值不直接传给用例而是先传给指定的fixture让fixture对这个值做加工然后fixture再把处理后的数据交给用例。什么场景会用到典型的场景是一个接口的参数在不同业务场景下需要不同的用户权限。你可以在parametrize里声明用户角色让fixture根据角色去登录不同的账号把对应的token加工好再传给用例import pytest pytest.fixture def auth_token(env_config, role): 根据角色登录不同账号获取token role_map { guest: {username: guest_user, password: Guest123}, normal: {username: normal_user, password: Normal123}, admin: {username: admin_user, password: Admin123}, } if role not in role_map: raise ValueError(f不支持的role: {role}) account role_map[role] url f{env_config[base_url]}/api/auth/login resp http_client.post(url, jsonaccount) return resp.json()[data][token] pytest.mark.parametrize(role, [guest, normal, admin], indirectTrue) def test_view_order(role, auth_token): 不同角色查看订单详情 resp http_client.get(/api/order/detail, params{order_id: 1001}, headers{Authorization: fBearer {auth_token}}) # 不同角色可能期望不同的返回结果indirectTrue的作用是先按role的具体值去找同名fixture调用它拿到token再把这个token作为fixture注入用例。这样可以避免在用例内部写一堆if-else判断角色逻辑。代码看着清爽扩角色的时候改role_map就行。3.3 参数化与fixture组合的坑别踩参数化跟fixture组合是有讲究的。fixture的参数注入是运行时解析parametrize的参数注入是收集阶段解析两者不能混为一谈。一个常见的错误是试图在parametrize里直接使用fixture的函数返回值# 错误示例 pytest.mark.parametrize(order_id, [create_order().get(order_id)]) def test_get_order(order_id): ...这段代码在pytest收集阶段就会报错因为create_order是fixture不能直接调用。正确姿势有两种要么用request.getfixturevalue在固定fixture中获取要么把parametrize的数据设计成“怎么生成数据”的说明而不是“已生成的数据”。前者适合你确实需要把fixture结果当作参数传递给其他用例的情况pytest.fixture def order_context(request): order_id request.getfixturevalue(create_order)[order_id] return order_id def test_get_order(order_context): resp http_client.get(f/api/order/detail, params{order_id: order_context}) assert resp.status_code 200这种写法多用于需要根据运行结果动态生成参数值的场景。大多数时候我更推荐把数据准备改成工厂模式在用例内部显式调用参数化只负责传入“不同场景的输入差异”这样逻辑更直观。4. 工程化配置环境切换、HTTP请求封装与日志前面聊了fixture和数据驱动但这套框架还缺少一个关键能力怎么在开发环境、测试环境、预发环境之间优雅地来回切换怎么统一处理请求日志不能用例一多日志全散落在终端定位问题像大海捞针。4.1 通过命令行参数动态切换环境很多人会在代码里写一个变量比如BASE_URL https://test.api...换环境就改代码。这个做法在一个人维护的小项目里能跑但多人协作时就会出事张三下午改了BASE_URL忘了改回来李四晚上跑测试结果全部打到预发环境去了。正确做法应该是环境通过命令行参数传入代码里不写死任何环境地址。pytest提供了pytest_addoption钩子可以在conftest.py里自定义命令行参数# conftest.py import pytest def pytest_addoption(parser): parser.addoption( --env, actionstore, defaulttest, choices[dev, test, staging], help指定测试环境: dev/test/staging ) pytest.fixture(scopesession) def env_config(request): env request.config.getoption(--env) import yaml with open(config/environments.yaml, r, encodingutf-8) as f: all_env yaml.safe_load(f) return all_env[env]跑用例的命令就变成pytest --envdev pytest --envtest pytest --envstaging环境彻底跟代码解耦。dev环境点错了不会影响test环境test环境的数据污染也不会带到staging。同一个框架所有人都用同一个命令行入口只是参数不同环境地址完全从config文件读取。4.2 requests.Session封装为所有请求加上公共逻辑很多人直接用requests.get()、requests.post()来写接口用例也没问题但你有没有遇到过这几个痛每个接口都要手动传headers带token、每次请求都要打印日志、网络抖动时希望自动重试、响应超时希望统统一套参数。用requests.Session做一个薄封装这些问题一次性解决。# common/http_client.py import requests import logging from tenacity import retry, stop_after_attempt, wait_fixed logger logging.getLogger(__name__) class HttpClient: def __init__(self, base_url, token_providerNone, timeout10): self.session requests.Session() self.base_url base_url.rstrip(/) self.timeout timeout self.token_provider token_provider def _build_headers(self): headers { Content-Type: application/json, User-Agent: pytest-api-testing } if self.token_provider: token self.token_provider() if token: headers[Authorization] fBearer {token} return headers def _log_request(self, method, url, **kwargs): logger.info(请求: %s %s, 参数: %s, 数据: %s, method, url, kwargs.get(params), kwargs.get(json)) def _log_response(self, resp): duration_ms int(resp.elapsed.total_seconds() * 1000) logger.info(响应: 状态码%s, 耗时%sms, 内容%s, resp.status_code, duration_ms, resp.text[:500]) retry(stopstop_after_attempt(3), waitwait_fixed(1), reraiseTrue) def request(self, method, path, **kwargs): url f{self.base_url}{path} kwargs.setdefault(headers, self._build_headers()) kwargs.setdefault(timeout, self.timeout) self._log_request(method, url, **kwargs) resp self.session.request(method, url, **kwargs) self._log_response(resp) if resp.status_code 400: logger.error(接口返回异常: %s %s - %s, method, url, resp.text) return resp def get(self, path, **kwargs): return self.request(GET, path, **kwargs) def post(self, path, **kwargs): return self.request(POST, path, **kwargs) def put(self, path, **kwargs): return self.request(PUT, path, **kwargs) def delete(self, path, **kwargs): return self.request(DELETE, path, **kwargs)这个封装里我用tenacity做了自动重试网络偶发抖动的场景下失败后自动重试3次每次间隔1秒。重试不是盲目使用只对网络层异常生效http状态码在400以上的响应本身已经返回了这时候不应该重试服务端反而会因为反复重试产生重复数据。所以这里只对连接异常、超时这类异常做重试。token_provider是一个回调函数本质上就是咱们前面说的login_token fixture里返回的token获取逻辑。请求封装不需要关心token是从哪里来的只要知道调用一个方法就能拿到最新的token就行这个解耦做得很干净。4.3 日志配置与allure报告让测试结果一目了然日志跟allure报告这两个配合起来排查效率能提升不少。pytest内置的logging集成可以让日志按用例输出到终端也可以输出到文件。pytest.ini里配置的log_cli相关参数就是干这个的。日志格式里带上时间戳、级别、消息跑用例的时候终端输出像流水账一样出问题能定位到是哪一步。allure报告在接口测试里的价值比UI测试更大因为接口测试数据量大、断言多失败时你需要知道具体是哪个接口、哪个参数、期望值是什么、实际值是什么。allure的allure.step装饰器可以把一大步拆成若干子步骤allure.attach可以把请求体、响应体直接挂到报告上。import allure allure.step(创建订单) def create_order_with_allure(http_client, payload): with allure.step(发送请求): resp http_client.post(/api/order/create, jsonpayload) with allure.step(校验响应): allure.attach(f请求参数: {payload}, namerequest, attachment_typeallure.attachment_type.JSON) allure.attach(f响应内容: {resp.text}, nameresponse, attachment_typeallure.attachment_type.TEXT) assert resp.status_code 200 return resp生成报告的命令也一并写进去在pytest.ini的addopts里配置好--alluredirreports/allure-results跑完后执行allure generate reports/allure-results -o reports/allure-html --clean allure open reports/allure-html这样团队成员拿到一份allure报告不仅有测试通过率、失败率还能在测试步骤里看到每一个请求的完整报文排查问题的时候不用再翻终端日志。5. 用例变多之后怎么跑得快、跑得稳接口测试用例上了300条以后如果不做优化全量回归至少要跑半小时以上。这个时间已经快到一个开发团队无法容忍的程度了。并行执行和失败重跑这两件事几乎是接口测试框架的必经之路。5.1 pytest-xdist并行执行线程与进程的选择pytest-xdist是官方生态里最成熟的并行方案。它支持-m pytest -n auto让pytest自动检测CPU核心数并启动相应数量的worker。参数可以指定为数字也可以指定为auto。但这里有个很多人容易忽略的坑pytest-xdist默认不是线程并行而是进程并行。每个worker是独立的Python进程session级别的fixture会在每个worker里分别执行。这意味着你如果有session级的共享状态比如token缓存、数据库连接池每个worker会各建一份不会互相共享。所以在设计fixture时要记住session级不是全局单例而是“每个worker里单例”。另外一个比较隐蔽的问题是如果多个worker同时执行用例而你的用例之间有数据依赖比如用例A创建的数据被用例B依赖那就一定会出问题。接口测试想要并行跑得稳必须先保证用例之间的独立性。如果用例之间确实存在依赖关系我给你三个参考方案把有依赖的用例合并成一个用例在同一个用例函数里按顺序执行多个接口调用。给有依赖的用例加上pytest.mark.run(ordern)用pytest-ordering控制执行顺序但这不是根本解法只能保证当前情况下不冲突。把共享的前置数据准备比如初始化订单放到模块级fixture中一次运行模块内的用例通过工厂模式每次生成自己的数据串行部分留在fixture里完成。以我实际经验来看并行带来的提速效果非常显著。8核心机器上300条用例从20分钟缩短到4分钟以内速度提升5倍左右但前提是你要先把用例间依赖彻底清理干净。5.2 失败重跑策略别让偶发问题毁掉一整晚的回归接口测试最让人抓狂的不是功能真的出了问题而是偶发性的失败网络抖动、服务端缓存未刷新、第三方回调延迟、测试环境定时任务占用了资源。一个偶发失败让整条回归链变红开发看到后会说“这个用例本来就没问题你们环境不稳定”你是解释也不是不解释也不是。pytest-rerunfailures插件可以解决这个问题。指定每条用例失败后的重试次数和间隔时间pytest --reruns 2 --reruns-delay 3--reruns 2表示最多重试2次--reruns-delay 3表示每次重试间隔3秒。也可以单独给某个用例设置pytest.mark.flaky(reruns2, reruns_delay5) def test_payment_callback(): ...但我要提醒一点重试是把双刃剑。如果接口本身有bug重试只会浪费时间和资源。我的经验是在回归测试中开启了全局重试1次但冒烟测试不开启重试。回归测试重试能滤掉环境偶发问题冒烟测试要求一次通过任何失败都必须立刻暴露。另外重试次数也不宜过多。一次接口测试如果连续重试3次还失败那大概率不是偶发问题而是真实缺陷或环境已经挂了再重试也没有意义不如尽早暴露出来。5.3 冒烟用例与全量用例分开跑用mark标记区分冒烟用例和回归用例已经是接口测试团队的标配做法了。在pytest.ini里已经声明了smoke和regression两个标记用例上打标pytest.mark.smoke def test_login_success(): ... pytest.mark.regression def test_create_order_after_login(): ...跑的时候按需选择# 只跑冒烟用例快速验证主流程 pytest -m smoke # 跑全量回归没问题 pytest -m smoke or regression我一般建议冒烟用例控制在20条以内全部是核心链路比如“登录、创建订单、支付、查询订单、退款”这条主流程上的关键接口。每天代码提交后开发先自跑冒烟只有冒烟通过才跑全量回归。冒烟用例跑完只需要2分钟回归用例跑完可以交给流水线后台处理。这样高频反馈和全面回归兼顾。6. Mock接口测试里的“替身演员”接口测试不是总能等到所有上游接口开发完毕。支付回调、短信发送、第三方物流查询、实名认证服务这些接口往往不在你的控制范围内或者环境不稳定。mock就是解决这类问题的通用方法。6.1 用requests-mock拦截HTTP请求requests-mock是专门为requests库设计的mock工具。它拦截的是requests.Session发送的请求所以在pytest里用起来很顺手。最基础的使用方式import requests_mock import pytest def test_payment_callback(): with requests_mock.Mocker() as m: m.post(http://third-party.pay.example.com/api/callback, json{status: success}) code run_payment_callback() assert code 200这里m.post注册了一个假的响应只要代码里向这个URL发送POST请求requests库直接返回配置好的json数据不会真正访问外部服务。整个过程毫秒级完成不受网络和环境状态影响。但真正的接口测试mock核心不是“返回假数据”而是“验证你的请求确实发对了”。mock方案里最容易被忽略的一点是你不仅要mock响应还要断言请求的参数。很多第三方接口失败的原因不是你的代码逻辑有问题而是请求参数格式不符合接口规范。所以mock时一定要加上对请求内容的验证import requests_mock def test_payment_callback_with_assert(): expected_payload None with requests_mock.Mocker() as m: m.post(http://third-party.pay.example.com/api/callback, json{status: success}) # 让下游代码执行支付回调 result execute_payment_callback(order_id1001, amount99.9) # 验证请求确实发出去了并且参数正确 history m.request_history assert len(history) 1 request_payload history[0].json() assert request_payload[order_id] 1001 assert request_payload[amount] 99.9 assert request_payload[sign] expected_sign这样你mock的就不只是一堆假数据而是一个完整的“替身演员”对请求严格校验对响应稳定可控。用这种方式做第三方接口的异常场景测试效果甚至比真实联调更好——因为你可以轻易模拟各种极端响应比如超时、500错误、返回空值真实环境里这些情况反而不容易触发。6.2 fixture中优雅地注入mock把mock跟fixture结合可以在整个测试模块里统一管理。下面的例子演示了如何在fixture中启动一个mock服务让下游模块的用例都能共用pytest.fixture def mock_payment_service(env_config): mock支付服务模拟第三方支付接口的各种响应 with requests_mock.Mocker() as m: base_url http://third-party.pay.example.com m.post(f{base_url}/api/callback, json{status: success}, status_code200) m.post(f{base_url}/api/callback_fail, json{status: failed}, status_code200) m.post(f{base_url}/api/callback_timeout, excrequests.exceptions.ConnectTimeout) yield m这样无论多少用例mock配置集中维护用例侧只用关心业务本身。6.3 mock过度的红线边界在哪mock虽然好用但一定要知道它的边界在哪。我见过最典型的反面案例某团队为了“让测试更稳定”把业务系统内部的接口也全部mock掉结果跑出来的测试除了验证“测试代码自己没写错”之外什么实际价值都没有。mock的核心原则是只mock你控制不了的外部依赖不mock被测系统自身的核心逻辑。如果你mock了被测系统自身的关键依赖那测试就变成了“自己验证自己”一旦系统内部真实逻辑有问题你的测试反而会因为mock把问题掩盖了。比如你在测“创建订单”这个接口你把“库存扣减”这个内部服务也mock掉了那就算创建订单的逻辑有问题你的测试也会通过因为mock帮你掩盖了错误。正确使用mock的边界是可以mock第三方支付回调、短信服务、外部实名认证、物流查询接口、消息推送。不应该mock被测系统自身的数据库操作、与自身业务模块的交互、缓存服务除非你在测缓存模块本身。7. 常见问题与排查技巧实录这部分内容来自我在多个项目里带测试团队的真实经验每个问题都对应过一次真实的事故只讲基础教程的人大概率不会告诉你。7.1 中文乱码与编码问题接口返回的中文乱码是个高频问题尤其是老项目没用UTF-8编码。requests库在读取响应内容时会根据header中的content-type推断编码如果服务端返回的编码声明不对requests可能用错误的编码解析导致text内容乱码。最简单的排查方式是打印resp.encoding通常乱码时这里的值要么是ISO-8859-1要么是None。解决方法是手动设置resp http_client.get(/api/user/info) resp.encoding utf-8 data resp.json()更稳妥的做法是在requests.Session上统一设置self.session.headers.update({Accept: application/json; charsetutf-8})如果你的服务端确实用了非UTF-8编码那就根据实际编码设置但这种情况越来越少见了。7.2 token失效与多环境数据污染token失效是接口测试里最痛的问题之一。一次性token、二小时有效期的token、每天凌晨刷新一次的token都会让你的用例时好时坏。处理思路有三层第一层在http_client的token_provider里做缓存token获取后存到变量里只有请求返回401时才重新获取。这个策略适合token有效期在几小时以上的场景。class TokenManager: def __init__(self, env_config, http_client): self.env_config env_config self.http_client http_client self._token None def get_token(self): if self._token is None: self._token self._login() return self._token def refresh_token(self): self._token None return self.get_token()第二层给你的http_client加一个401自动重试机制当某个请求返回401时清除当前token并重新登录一次再次发送这个请求。这一招在token临失效的场景下特别管用。第三层多环境同时跑用例时session级fixture里一定要清理登录态。有些框架的登录接口是全局唯一的环境A登录成了环境B再用同一个token访问就会串环境。所以在fixture里指定环境参数每个环境用自己的账号登录尽量避免“全局一个token”的写法。7.3 fixture执行顺序的隐性依赖fixture之间可以互相依赖但有个坑fixture的执行顺序不一定按照你代码里的声明顺序。比如你同时写了一个fixture叫login另一个fixture叫order用例声明依赖order和loginpytest会通过依赖树判断执行顺序但如果你在fixture内部偷偷修改了另一个fixture依赖的数据就可能触发诡异的时序问题。实战中我建议fixture之间的依赖尽量控制在“单一链条”上不要出现A依赖B、B依赖C、C又依赖A的环形依赖。pytest虽然能检测出环形依赖并报错但这种设计本身就说明你耦合得太深了。7.4 常见报错速查表报错信息大概率原因处理方式Fixture named xxx not found引用了不存在的fixture或者拼写错误检查conftest.py是否定义了该fixture检查路径层级Function not implementedfixture只写了函数名没加return/yieldfixture必须要有yield或return返回控制权ScopeMismatch: You tried to access function-scoped fixture ... from session fixturesession级fixture依赖了function级fixture提升被依赖fixture的作用域到sessionWorker gw0 crashed while runningworker进程崩溃常见于内存溢出或超大数据量尝试减少并行worker数量检查是否代码中申请了过多内存1 failed, 0 passed in 5.0s收集阶段或者执行阶段出现致命异常优先看报错堆栈最后几行打开--tblong看完整错误中文乱码编码问题手动指定resp.encoding或者修改server端header7.5 接口测试定位问题的最佳姿势最后分享一个排查接口测试失败问题的固定流程我用了很长时间效率很高。当一条用例失败时按这个顺序排查看响应状态码。500了大概率是服务端报错需要查服务端日志404了先确认接口路径是否正确、环境是不是连错了。看业务码。很多接口即使HTTP 200返回业务码也可能表示失败比如code40001表示参数错误。这时候重点看响应体里面的message字段。看请求参数。用allure报告或日志里的请求详情逐项对比参数和接口文档。看是否偶发。重新执行一次这条用例如果通过很可能就是环境或网络问题如果稳定复现才考虑代码缺陷。看数据状态。确认前置数据是否被之前跑的用例修改过。这个流程走下来大部分问题都能在5分钟内定位到根因。关键是每一步的操作都要有据可依报错信息、请求日志、响应日志、用例数据平时就要把这些完整记录下来排查的时候才不会抓瞎。8. 一个坚持了很久的小习惯写到这里最后分享一个我个人在接口测试项目里一直坚持的小习惯每次新增一个测试模块先在根目录的README里更新模块说明和环境依赖然后在testcases目录建好对应的子目录和conftest.py最后才写用例。这样做的好处是不管过了多久任何人接手这套测试代码都能通过目录结构和文档快速理解项目全貌不用靠某个人“口口相传”。代码会变、数据会变、接口会变但清晰的工程结构和规范的维护习惯才是一套接口测试框架能持续发挥价值的根基。如果你现在正准备把接口测试从“临时脚本”升级成“正式框架”我建议你回过头去看一眼自己的目录结构再看看conftest里的fixture作用域先别急着写用例把骨架搭对了后面每一步都会顺很多。
返回列表