接口测试系列:接口自动化测试 pytest框架(三)
这篇继续聊接口自动化测试。前面两篇如果都读过了,应该已经知道pytest是怎么把一类接口的用例组织起来的,也知道基础断言、fixture大概怎么回事。但很多朋友卡在一个地方:单跑一两个接口很顺,一旦接口变多、数据要复用、环境要切换,用例结构就乱了,维护成本直接起飞。这篇就把工程化落地的事一次性讲透,重点说pytest框架里真正决定用例好不好维护的几个核心机制,包括conftest全局复用、parametrize参数化、token管理、数据驱动、报告生成和失败重试。看完之后,你手上那套接口自动化测试的代码,应该能直接从“能跑”升级到“能进CI、能长期维护”。适合已经写过一些pytest用例、想搭一套正规接口自动化测试框架的同学,也适合被一堆重复代码折磨得想重构的人。
1. 内容整体设计与思路拆解
1.1 系列前两篇做了什么事,第三篇补齐哪块
先说清楚这个系列走到哪一步了。前两篇大概率已经把pytest的发现规则、断言写法、基础fixture讲得差不多了。那些东西是地基,没有它们后面什么都没法聊。但只靠那些基础能力去写接口自动化测试,你会很快发现一个尴尬局面:每个测试文件都要重新登录、重新拼参数、重新读配置,公共逻辑堆在顶层函数里,改一个环境地址要全局搜索替换。
第三篇就是来解决这个尴尬的。目标很直接:让测试代码具备工程化能力。说得细一点,是四件事。第一,用conftest.py把全局资源(请求客户端、token、日志、配置)统一管理起来,用例文件里不再重复造轮子。第二,用parametrize把一条用例扩展成一组用例,数据量上去之后代码量不上去。第三,把接口关联、动态参数、数据驱动这些真实业务场景里躲不开的问题,用一套可复用的方案落地。第四,把报告、重试、环境切换这些“上了CI才知道有用”的东西提前配好。
所以我建议把这篇当作一次“脚手架搭建”来看。前两篇教你怎么写单个用例,这篇教你怎么让一百个用例还能优雅地活着。
1.2 为什么接口自动化测试要选pytest而不是其他
这个话题网上讨论很多,但落到实际项目里,我的判断标准其实很朴素:团队能不能低成本地理解它、插件生态够不够、失败之后能不能快速定位。pytest在这三件事上都做得不错。
对比一下。unittest是标准库,但它那套类继承结构和广泛的setUp/tearDown约定,写多了会让人忍不住吐槽:一个用例要关心类的生命周期,接口测试里真的没那么多人愿意写一个TestCase类。Robot Framework上手门槛低,但当你需要自定义断言逻辑、动态参数组合、灵活控制执行顺序的时候,封装一层又一层,反而比pytest难维护。pytest的思路不一样,它把测试函数当作普通函数来处理,fixture通过依赖注入自动传参,用例只管声明“我需要什么”,至于这个依赖怎么创建、什么时候销毁,由fixture体系管理。这个抽象方向对了,写起来就顺手很多。
另外一个点是插件生态。pytest的插件不夸张地说覆盖了自动化测试的大部分需求:参数化、依赖控制、报告、重试、顺序控制、并发执行。这些东西不是哪个公司内部的封装的私有能力,而是社区沉淀出来的通用方案,踩坑资料也多。做接口自动化测试,选pytest在当下不是最优解,但一定是不容易错的选择。
1.3 工程目录怎么规划,才能支撑多接口多环境
一个清晰的项目目录,比任何设计模式都重要。目录结构是框架的骨架,如果连目录都乱,后面的代码怎么组织都是别扭的。
给你一个我实测比较好用的目录结构,你可以按需裁剪:
api_auto_test/ ├── config/ │ ├── __init__.py │ ├── settings.py │ └── env.yaml ├── common/ │ ├── __init__.py │ ├── request_client.py │ ├── context.py │ ├── read_data.py │ └── log.py ├── testcases/ │ ├── __init__.py │ ├── conftest.py │ ├── test_user.py │ └── test_order.py ├── data/ │ ├── user.yaml │ └── order.yaml ├── reports/ │ └── allure-results/ ├── logs/ ├── pytest.ini └── requirements.txt几个关键决策说一下。testcases只放用例文件,不放公共逻辑,公共逻辑全部下沉到common。这样新人进来之后,第一眼就知道“我要看用例就看testcases,我要改请求封装就看common”,心智负担小很多。data目录放测试数据文件,这是数据驱动的基础。reports和logs生成出来的东西不进版本库,这一条建议放到.gitignore里。pytest.ini放pytest配置,包括测试路径、参数、标签,关于pytest.ini的用法后面会讲到。
这里有个细节值得多提一句:common里那层request_client.py,不是简单把requests再封装一层,而是要把接口测试需要的通用逻辑都收进去,比如统一打印请求日志、统一添加上下游关联变量、统一处理response断言。这层是做接口自动化测试的核心资产。
2. 核心细节解析与实操要点
2.1 fixture的依赖注入,测试函数的状态来源
fixture是pytest里最值得花时间理解的概念,没有之一。它的本质是依赖注入:测试函数声明自己需要一个参数,pytest就去查找同名的fixture,执行它并把返回值传给测试函数。你不用在测试函数里关心这个fixture是怎么创建的,也不用关心它什么时候释放。
先看一个最简单的例子:
import pytest @pytest.fixture def user_token(): # 模拟登录获取token return "fake-token-123456" def test_get_user_info(user_token): assert user_token.startswith("fake-token")这个例子太简单,但已经能看出fixture的意义:test_get_user_info如果直接自己发请求拿token,那这个用例就既要关心“怎么登录”又要关心“怎么验证用户信息”,职责混乱。通过fixture,登录逻辑被抽离了,测试函数只关心自己的业务验证。
fixture有四个常见参数,选择的时候要想清楚。
- scope:控制fixture的生命周期。function级别的fixture每个测试函数执行一次,class级别每个类执行一次,module级别每个模块执行一次,session级别整个测试会话只执行一次。
- autouse:设为True时,该fixture会被同作用域下的所有测试自动调用,不需要在参数里声明。
- params:给fixture传参数,一个fixture参数值列表会生成多个fixture实例。
- yield:在yield之前的代码是setup,yield后面的代码是teardown。
其中scope的选择最容易犯错。比如登录token这种资源,理论上用session级别最合适,但如果你没搞清楚session级别fixture的缓存机制,就可能出现“第一个用例登录成功,后面的用例token过期”这种问题。这个问题我们在第三部分详细展开。
2.2 conftest.py:跨文件复用,而不是写一堆基类
如果说fixture是pytest的依赖注入机制,conftest.py就是把这个机制“全局化”的关键。conftest.py是一个特殊文件,pytest会从测试目录开始向上查找所有conftest.py并自动加载其中的fixture。你不需要显式import它,定义在conftest.py里的fixture会自动对同目录及子目录下的测试生效。
这个特性直接解决了公共逻辑复用的问题。以前写unittest的时候,公共逻辑要么写一个基类然后让TestCase继承,要么写一个工具函数到处import。这两条路都不够优雅。基类继承会引入类层次结构,改基类就可能影响所有子类,而且新人很难一眼看出基类到底做了什么。工具函数到处import的问题是:你根本没法方便地在函数里注入“带状态的资源”,比如一个已经登录好的session,你总不能每个函数都手动传一遍吧。
conftest.py的解决方式是声明式复用。举个例子:
# testcases/conftest.py import pytest from common.request_client import RequestClient @pytest.fixture(scope="session") def client(env_config): return RequestClient(base_url=env_config["base_url"])在testcases目录下所有测试函数里,直接写def test_xxx(client)就能拿到一个已经初始化好的请求客户端。这个client是session级别的,所有用例共用同一个连接池,跑起来快很多。
conftest.py还有一个作用:放钩子函数(hook)。比如你想在每个用例执行完打印响应日志,就可以在conftest.py里写pytest_runtest_makereport钩子。这是unittest完全做不到的扩展方式。钩子函数这块内容不少,但我们这篇文章先聚焦fixture,后续可以单独开一篇讲pytest钩子。
2.3 parametrize参数化:一条用例撑起一个数据集
接口测试里最典型的场景是:同一个接口,多组入参,每个入参都对应不同的期望结果。如果复制粘贴十遍用例,代码冗余不说,报告里还看不出是哪一组数据挂了。这时候就必须用parametrize。
import pytest @pytest.mark.parametrize("username,password,expected_code", [ ("admin", "123456", 200), ("admin", "wrong", 401), ("", "123456", 400), ("admin", "", 400), ], ids=["正常登录", "密码错误", "用户名为空", "密码为空"]) def test_login(username, password, expected_code): resp = do_login(username, password) assert resp.status_code == expected_code这里直接给参数化加了ids,这样在测试报告中看到的是“正常登录”“密码错误”这样的名字,而不是“login[0]”这种无意义编号。别小看这一个参数,报告可读性在排障时能帮你省不少时间。
parametrize还支持多组参数组合,也就是笛卡尔积。比如测试搜索接口,要分别测试关键词、页码、排序方式,就可以组合出几十个用例:
@pytest.mark.parametrize("keyword", ["手机", "电脑", ""]) @pytest.mark.parametrize("page", [1, 2]) @pytest.mark.parametrize("sort", ["asc", "desc"]) def test_search(keyword, page, sort): pass这样就能生成12条用例。不过要注意:组合用例的数量增长很快,实际项目中要控制组合维度,不然跑一次回归时间会变得不可控。
3. 实操过程与核心环节实现
3.1 请求客户端:把session复用起来
做接口自动化测试,大多数人会直接requests.get/post,这没问题,但一旦用例变多就会遇到两个麻烦:每次请求都要写header、token,重复代码满天飞;requests每次创建新连接,没有复用连接池,性能浪费。更关键的是,你没地方统一打印日志,出了问题排查起来很被动。
所以第一步,封装一个请求客户端。核心是使用requests.Session。
# common/request_client.py import requests from common.log import logger class RequestClient: def __init__(self, base_url="", token=""): self.session = requests.Session() self.base_url = base_url self.headers = { "Content-Type": "application/json", "Authorization": f"Bearer {token}" } def request(self, method, path, **kwargs): url = self.base_url + path kwargs.setdefault("headers", self.headers) response = self.session.request(method, url, **kwargs) logger.info(f"{method.upper()} {url} -> {response.status_code}, time={response.elapsed.total_seconds():.3f}s") if response.headers.get("content-type", "").startswith("application/json"): logger.info(f"response: {response.text}") return response def get(self, path, **kwargs): return self.request("GET", path, **kwargs) def post(self, path, **kwargs): return self.request("POST", path, **kwargs) def update_token(self, token): self.headers["Authorization"] = f"Bearer {token}"用Session的好处是TCP连接会被复用,Cookie可以自动携带,而且可以在一个session范围内统一设置headers。日志打印是刚需,每次请求都记录状态码和耗时,排查问题的时候能快速定位是哪一步慢、哪一步挂了。
注意这里对json请求体的处理。如果你传json参数,requests会帮你在请求头里加上Content-Type: application/json,但如果你已经在headers里写死了,就要留意优先级问题。实际上requests对json参数的处理是在调用的那一刻动态设置Content-Type的,所以不会冲突,但如果你直接用data传字符串,就必须自己保证content-type是对的。
3.2 token管理:登录一次,全局有效
接口测试里最常见的依赖是token。大多数业务接口需要登录后拿到token才能访问。token如果每个用例都登录一次,性能太差;如果生成后整个测试过程一直用,又怕过期。所以我把token管理设计成session级别的fixture,整个pytest进程只登录一次。
# testcases/conftest.py import pytest from common.request_client import RequestClient from common.context import Context @pytest.fixture(scope="session") def env_config(): # 具体读取逻辑见3.5 return read_env("test") @pytest.fixture(scope="session") def context(): return Context() @pytest.fixture(scope="session") def client(env_config, context): client = RequestClient(base_url=env_config["base_url"]) # 登录并保存token login_resp = client.post("/api/login", json={ "username": env_config["username"], "password": env_config["password"], }) assert login_resp.status_code == 200 token = login_resp.json()["data"]["token"] client.update_token(token) # 把token存入全局上下文,其他fixture也能用 context.set("token", token) return client这里有个小设计很关键:Context类。它是一个全局上下文容器,用来保存接口之间的关联变量。token、user_id这类数据都可以放进去。为什么要自己做这么一层?因为pytest的fixture返回值默认是不可变的,你不能在多个fixture之间共享一个可变对象然后随便改它。Context类就是我们的“全局变量区”。
# common/context.py class Context: def __init__(self): self._data = {} def set(self, key, value): self._data[key] = value def get(self, key, default=None): return self._data.get(key, default)3.3 接口关联:上一个接口的返回值怎么给下一个用
接口关联是业务型接口自动化测试的必修课。比如下单接口需要用户ID,而用户ID是创建用户接口的返回值。这种关联关系如果靠用例之间手动传参,代码就会写成一坨。更通用的做法是:使用上下文变量引用,然后请求前做替换。
做法不复杂,在RequestClient的request方法里加一个步骤:发送请求前,把path、json、params、headers里的${key}这种占位符,替换成Context里存的值。这个思路和JMeter里的用户自定义变量、Postman里的环境变量是一样的思路。
import re class RequestClient: def __init__(self, base_url="", token="", context=None): self.context = context def _replace_context(self, data): if isinstance(data, str): def replacer(match): key = match.group(1) value = self.context.get(key) if value is None: raise ValueError(f"context中未找到变量: {key}") return str(value) return re.sub(r"\$\{(\w+)\}", replacer, data) elif isinstance(data, dict): return {k: self._replace_context(v) for k, v in data.items()} elif isinstance(data, list): return [self._replace_context(item) for item in data] return data def request(self, method, path, **kwargs): path = self._replace_context(path) if "json" in kwargs: kwargs["json"] = self._replace_context(kwargs["json"]) if "params" in kwargs: kwargs["params"] = self._replace_context(kwargs["params"]) if "headers" in kwargs: kwargs["headers"] = self._replace_context(kwargs["headers"]) # ...发起请求这样在写用例的时候,可以很形象地表达关联关系:创建用户接口返回user_id后,用context.set("user_id", user_id)存起来。下单接口测试数据里直接写"userId": "${user_id}",执行时自动替换。这个设计能极大减少用例代码量,而且让测试数据文件更像“数据”而不是“逻辑”。
这里要提醒一个细节:requests.Session的请求是无状态的,你改client.headers不会影响之前已经发出的请求,但如果你在并发场景下修改context,就要考虑竞态了。好在大多数接口自动化测试是顺序执行,并发是后面用pytest-xdist时才需要考虑的事。
3.4 数据驱动:从yaml读取测试数据,让用例只看数据不看逻辑
写到这一步,用例函数已经比较清爽了,但还有一个问题:测试数据写死在代码里,每加一条用例就要改代码,这不符合测试数据与测试逻辑分离的原则。解决方案是数据驱动:把测试数据写到yaml文件,用例函数用parametrize把数据加载进来。
yaml是我最推荐的数据格式,它可读性好、层级清晰,比Excel好在能版本管理,比json好在写起来省引号和逗号。
# data/order.yaml - name: "正常创建订单" api: /api/order/create method: POST data: userId: "${user_id}" productId: "1001" count: 2 expected: status_code: 200 code: 0 - name: "商品数量为0" api: /api/order/create method: POST data: userId: "${user_id}" productId: "1001" count: 0 expected: status_code: 200 code: 10001读取yaml的函数放common/read_data.py:
# common/read_data.py import yaml import os def read_yaml(file_path): with open(file_path, encoding="utf-8") as f: return yaml.safe_load(f) def load_test_data(file_name): data_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") return read_yaml(os.path.join(data_dir, file_name))然后在用例文件里加载数据:
# testcases/test_order.py import pytest from common.read_data import load_test_data order_cases = load_test_data("order.yaml") @pytest.mark.parametrize("case", order_cases, ids=[c["name"] for c in order_cases]) def test_create_order(client, case): resp = client.request(case["method"], case["api"], json=case["data"]) assert resp.status_code == case["expected"]["status_code"] body = resp.json() assert body["code"] == case["expected"]["code"]这里有一个需要特别提醒的点:parametrize的参数列表是在收集阶段就确定的,所以load_test_data的执行发生在模块导入时。这本身没问题,但意味着如果你在yaml文件里用了${user_id}这种占位符,它仅仅是一个字符串,真正执行时才会被替换。所以上面的用例里,创建订单数据里的${user_id}会在请求发出前被RequestClient._replace_context替换成真实值。这个设计的关键在于:占位符替换发生在request方法内部,而不是数据读取阶段,所以parametrize时data里还是占位符,完全不影响。
把这条讲透:为什么不在读取yaml的时候就直接替换?因为上下文里的值依赖前面用例的执行结果,而parametrize是收集阶段就执行的,收集阶段还没有执行任何用例,当然拿不到user_id。所以替换时机必须后移到请求阶段,这个顺序不能反。
3.5 环境切换:一套代码,多套环境随便切
测试环境、预发布环境、本地环境,地址不同、账号不同。如果改一次环境就要改代码,那框架离淘汰就不远了。
环境配置放在config/env.yaml:
# config/env.yaml test: base_url: "http://test-api.example.com" username: "test_user" password: "test_pass123" pre: base_url: "http://pre-api.example.com" username: "pre_user" password: "pre_pass123"读取逻辑:
# config/settings.py import os import yaml _current_env = os.getenv("TEST_ENV", "test") def get_env_config(): config_path = os.path.join(os.path.dirname(__file__), "env.yaml") with open(config_path, encoding="utf-8") as f: config = yaml.safe_load(f) return config[_current_env]运行的时候指定环境:
TEST_ENV=pre pytest -qWindows下是set TEST_ENV=pre && pytest -q。这个设计简单,但非常有效,它让环境切换变成启动参数而不是代码改动。
3.6 报告与重试:工具链组合,减少人工盯盘
接口自动化测试跑在CI里,没有人会盯着终端看输出,所以报告和失败重试是必需品。
报告我推荐用allure,虽然配置比pytest-html繁琐一点,但展示效果和数据丰富度好得多。基本用法是先装插件,然后执行时指定结果目录:
pip install allure-pytest pytest-rerunfailures pytest --alluredir=reports/allure-results --reruns 2 --reruns-delay 1用--reruns 2的意思是失败后最多重试2次,--reruns-delay 1表示每次重试间隔1秒。采集完结果后,用allure命令行生成报告:
allure generate reports/allure-results -o reports/allure-report --clean重试机制对接口测试来说非常实用,尤其是下游服务偶发超时这种问题,重试一次可能就过了。但注意一点:不是所有失败都适合重试,断言失败(比如返回码和期望不一致)重试多少次都不会变,这时候重试只会拖慢测试运行时间。所以重试次数不要设太高,一般2次足够。
使用allure之后,还可以通过装饰器给用例附加更丰富的描述信息,比如功能模块、严重级别、接口地址。
import allure @allure.feature("订单模块") @allure.story("创建订单") @allure.title("正常创建订单") @pytest.mark.parametrize("case", order_cases, ids=[c["name"] for c in order_cases]) def test_create_order(client, case): pass这样在allure报告里就能按功能模块筛选用例,排障时直接按模块归拢,不用一个个用例翻。别小看这种元信息,项目用例过千之后,筛选效率决定了排障效率。
4. 常见问题与排查技巧实录
4.1 用例执行顺序不可控,怎么办
pytest默认按模块文件名排序,模块内按函数定义顺序排序,但这个顺序很多时候不是你期望的业务顺序。有些同学喜欢通过用例名加数字前缀(test_01_xxx、test_02_xxx)来控制顺序,这是一种可行方案,但很容易让用例名变得很难看。
我更推荐的方案是:从设计上消除用例间的顺序依赖。每个用例都应该可以独立运行,而不是“上一个用例必须先生成数据”。接口关联通过context来解耦,而不是通过用例执行顺序来保证。如果非要有顺序,比如必须先有用户才能测订单,那应该在fixture层解决,比如在创建订单的用例里依赖一个created_user的fixture,由fixture去创建用户,而不是依赖test_create_user先执行。
如果实在要控制顺序,用pytest-order插件:
pip install pytest-order@pytest.mark.order(1) def test_login(): pass @pytest.mark.order(2) def test_create_order(): pass但请你相信我,顺序依赖是测试套件里最容易被忽视的定时炸弹。今天加一个用例、明天调整一个用例,顺序可能就乱了,然后莫名奇妙挂掉。尽量在框架设计层面避免它。
4.2 session级fixture的数据污染
这是我在实际项目中踩过最深的坑。一个session级别的fixture被设计成返回一个可变对象(比如列表、字典),结果某个测试函数往里加了一条数据,后续所有用到这个fixture的用例都受影响。表面上看是某条用例挂了,实际上是被前一个用例污染了。
解决方案有几个方向。fixture返回的对象尽量是不可变的,或者请求客户端内部不要暴露可变状态。如果确实需要共享可变状态,比如共享的用户信息字典,那就在使用侧做一个copy:
def test_update_user(user_info): local_info = dict(user_info) # 拷贝一份 local_info["name"] = "new_name" ...另外,session级别的fixture在设计时要考虑一个隐含假设:这个fixture内部的连接、token、状态,在整个测试会话中是单例且稳定的。如果某个用例需要特殊状态,就单独建一个function级别的fixture,不要复用session级的那个对象。
4.3 断言失败时缺少响应日志,排查全靠猜
这是新人最容易犯的错。断言一句assert resp.status_code == 200,一旦失败只会看到“assert 500 == 200”,完全没有上下文。你怎么知道是哪个接口、什么参数、响应体是什么?
所以我在RequestClient里统一打印了请求日志,但在断言层还需要再补一层。推荐用pytest的断言钩子pytest_assertrepr_compare,在conftest.py里自定义断言失败时的输出。但更简单实用的方式是:断言前先打印响应内容,或者用allure的attach把请求和响应挂在报告里。
import allure def assert_response(resp, expected_status_code): if resp.status_code != expected_status_code: allure.attach(resp.text, "响应体", allure.attachment_type.TEXT) raise AssertionError(f"期望状态码 {expected_status_code},实际 {resp.status_code},响应体: {resp.text}")这样失败时,报告里能看到请求失败时的原始响应,排查效率直接翻倍。这个思路要往框架深处走:让所有断言失败都自动带上日志上下文,而不是依赖每个用例的作者自觉。
4.4 参数化数据量过大,报告冗长难排查
组合参数化生成几百条用例,报告里一长串,看一眼就头疼。我的处理方式是给每个参数化用例都加清晰的ids,并在参数化数据里包含“业务名称”字段,这样报告能直接看出是哪条业务场景挂了。另外,如果参数组合特别多,可以给用例加上allure的layer和feature标签,按业务模块筛选,而不是在几百条用例里硬翻。
还有一个经验:数据驱动时,把预期结果也放到数据文件里,但预期字段要设计得合理。不要只存一个status_code,要存业务码(code)、关键返回字段,这样断言才能覆盖到接口的真实业务含义,而不仅仅是“没报错”。我见过很多接口自动化测试跑着全绿,但接口逻辑已经坏了,就是因为断言只比较了HTTP状态码,接口即使返回业务错误也是200,所以永远绿。断言必须落到业务字段上。
5. 给实战项目的几点补充建议
前面把框架的骨架搭起来了,这里再补一点实战中才意识到的细节。
pytest.ini是pytest的配置文件,建议在项目根目录维护一份。它能帮你统一测试路径、命令行参数、标签规则。比如这样:
# pytest.ini [pytest] testpaths = testcases python_files = test_*.py python_classes = Test* python_functions = test_* addopts = -ra --strict-markers markers = smoke: 冒烟测试标记 regression: 回归测试标记--strict-markers会强制要求标记必须先在pytest.ini里声明,这样就不会出现拼写错误导致的标记失效。-ra参数会在测试结束后汇总所有失败原因,跑长套件的时候很有用。
日志方面,我习惯在common/log.py里用logging模块封装一个带文件输出的logger。日志级别在本地调试时设置DEBUG,CI上设置INFO,避免日志刷屏。请求日志和断言日志分开看,方便定位。
最后说说并发执行。用例多起来之后,单线程跑可能就很慢了。pytest-xdist可以用多进程并行跑,但前提是测试用例之间没有共享状态,或者共享状态做了并发安全的处理。我在实际项目里是先保证用例独立性,然后才上并发,否则跑起来就是灾难。并发下context的写入要注意加锁,这个如果后面用到,可以单独讲一次。
再补一个容易被忽略的点:requests里timeout一定要设置。不设置timeout的话,某个接口挂起时,整个测试会一直卡着,CI任务永远不会结束。设置timeout之后,接口长时间无响应会被判定为失败,而不是无限等待。这个看着是个小事,但线上出过事故,教训深刻。
response = self.session.request(method, url, timeout=10, **kwargs)如果你想在yaml数据里也支持timeout配置,可以在request方法里优先使用kwargs里传入的timeout,没有的话再用默认值。这样灵活性更高。
写在最后
做了这么多接口自动化测试的项目,我个人最大的体会是:pytest框架本身只是一个执行引擎,真正的核心是围绕它设计的工程化配套。fixture管资源,parametrize管数据,conftest管复用,context管关联,日志和报告管排查。这五件事想清楚,一套接口自动化测试框架的基本盘就稳了。
最后再分享一个小技巧:框架搭好之后,不要把所有字段都放进配置文件或数据文件里。配置文件只放环境相关的内容(地址、账号),业务相关的数据放数据文件,代码相关的逻辑放common。这样分层之后,测试人员改数据不需要碰代码,开发人员改逻辑不会影响数据,两者职责清晰。
这个系列后面可以接着聊pytest的插件开发、并发执行、与CI工具集成,也可以聊聊如何把接口自动化测试的请求录制下来生成用例。有具体想了解的方向,可以在评论里一起讨论。