登录接口自动化测试:会话、断言、数据隔离与超时

📅 2026/8/26 21:24:13
登录接口自动化测试:会话、断言、数据隔离与超时
接口自动化刚开始写时很容易把用例简化成“发一个请求再断言状态码等于200”。这种写法能证明接口在当前输入下返回了成功响应却回答不了更多问题Token能不能真的访问受保护资源错误密码有没有稳定的业务错误码退出后旧Token是否立即失效服务迟迟不响应时客户端会不会一直等待。本文启动一个本地HTTP接口服务用Pytest和Requests完成注册、登录、鉴权、退出和超时验证。测试通过真实TCP连接访问接口不依赖外部公共服务也不使用框架内部的测试客户端重点放在请求组织、断言层次和数据隔离上。一、先明确登录接口要验证什么登录成功并不等于整条鉴权链路正确。一个较完整的验证过程至少包含五步创建账号、登录获取Token、携带Token访问资料接口、退出登录、再次使用原Token访问。这五步分别验证不同状态注册成功返回201登录成功返回200和Bearer Token有效Token可以访问资料接口退出成功返回204响应体为空原Token再次访问资料接口时返回401。如果只检查登录接口本身的200即使服务端生成的Token无法使用或者退出接口没有真正撤销Token用例依然会显示通过。因此登录测试的核心不是一个请求而是Token从创建、生效到失效的状态变化。二、环境与项目结构本次运行环境如下项目版本操作系统Windows 10Python3.12.13pytest9.1.1Requests2.34.2FastAPI0.141.1Uvicorn0.52.3第三方依赖安装在本篇独立的.venv中。项目结构如下showcase/ ├─ src/ │ ├─ auth_api/ │ │ ├─ app.py │ │ └─ store.py │ └─ api_client.py ├─ tests/test_auth_api.py ├─ examples/test_wrong_status_expectation.py ├─ tools/capture_responses.py ├─ outputs/response_samples.json ├─ conftest.py ├─ pyproject.toml └─ requirements.txtauth_api提供本地登录接口api_client.py封装Requests会话和默认超时conftest.py负责启动服务、创建账号和清理数据。这样测试文件只需要描述请求行为和预期结果。本地服务使用内存保存账号和Token密码没有加密。它用于验证HTTP测试组织方式不是生产鉴权实现。真实系统还需要密码哈希、Token签名与过期、持久化、限流、审计和密钥管理。三、为什么要封装Requests Session多个请求访问同一服务时可以使用requests.Session保存公共请求头并复用底层连接。本文将基础地址、Session和超时放进一个客户端对象class AuthApiClient: def __init__( self, base_url: str, *, timeout: tuple[float, float] (1.0, 1.0), ) - None: self.base_url base_url.rstrip(/) self.timeout timeout self.session requests.Session() ​ def login(self, username: str, password: str) - requests.Response: return self._request( POST, /api/login, json{username: username, password: password}, ) ​ def use_token(self, token: str) - None: self.session.headers[Authorization] fBearer {token}封装的目的不是隐藏所有Requests细节而是统一容易遗漏的公共配置。例如所有请求都通过_request()补上超时避免某条新用例忘记设置。登录成功后use_token()把Authorization头写入当前Session后续资料、退出和删号请求会自动携带相同Token。Requests默认不会主动超时。如果接口没有返回数据未设置timeout的请求可能等待很久。接口测试通常不应该把“无限等待”当作默认行为因此这里从一开始就给出连接超时和读取超时。四、用fixture启动服务并隔离账号数据测试会话开始时api_base_url选择一个本机空闲端口在后台线程中启动Uvicorn并轮询/health确认服务已经可用。整个测试集共用这个服务账号数据则按测试隔离pytest.fixture def registered_user(api_client: AuthApiClient) - Iterator[ApiUser]: user ApiUser( usernamefapi_user_{uuid4().hex[:8]}, passwordsafe-pass-2026, ) response api_client.register(user.username, user.password) assert response.status_code 201 ​ yield user ​ login_response api_client.login(user.username, user.password) if login_response.status_code 200: api_client.use_token(login_response.json()[access_token]) api_client.delete_current_user()每条需要账号的测试都会生成不同用户名结束后重新登录并删除账号。这样做比所有用例共用test_user更稳定并发执行时不容易产生用户名冲突某条用例修改会话状态也不会污染其他用例。清理逻辑放在yield之后即使断言失败已经建立的fixture仍会进入teardown。与此同时删号接口会撤销该账号关联的全部Token避免只删除用户记录却留下可用会话。五、成功用例要证明Token可用登录成功测试不只检查状态码还检查Token格式、类型、有效时间并立即访问受保护接口def test_login_returns_a_usable_bearer_token( api_client: AuthApiClient, registered_user: ApiUser, ) - None: login_response api_client.login( registered_user.username, registered_user.password, ) ​ assert login_response.status_code 200 body login_response.json() assert body[token_type] bearer assert body[expires_in] 3600 assert TOKEN_PATTERN.fullmatch(body[access_token]) ​ api_client.use_token(body[access_token]) profile_response api_client.profile() assert profile_response.status_code 200 assert profile_response.json() { username: registered_user.username, status: active, }这里没有把随机Token硬编码成某个固定字符串只检查它满足当前约定的格式。随后用这个Token获取用户资料能够进一步证明Token不是“看起来像Token的无效字段”。本次采集到的响应已经隐藏真实Token接口断言可以分成几个层次状态码判断请求结果类别响应体确认字段和业务错误码响应头检查鉴权约定后续请求验证状态变化超时断言处理无响应情况。六、负向用例不能只换一组密码错误密码和密码大小写变化都应返回401同时携带WWW-Authenticate: Bearer响应头并返回稳定的业务错误码pytest.mark.parametrize( (password, expected_code), [ pytest.param(wrong-pass, INVALID_CREDENTIALS, idwrong-password), pytest.param(SAFE-PASS-2026, INVALID_CREDENTIALS, idcase-sensitive), ], ) def test_login_rejects_invalid_passwords( api_client, registered_user, password, expected_code, ) - None: response api_client.login(registered_user.username, password) ​ assert response.status_code 401 assert response.headers[WWW-Authenticate] Bearer assert response.json()[detail][code] expected_code除此之外本文还验证了三类不同问题请求体缺少密码返回422重复注册返回409未携带Token访问资料接口返回401。这些状态码不能混为一谈字段校验失败、资源冲突和鉴权失败发生在不同阶段也应该有可区分的响应。负向测试中不宜一开始就调用response.raise_for_status()。它会把4xx响应转换为HTTPError如果用例只断言“抛出了HTTPError”就无法确认接口究竟返回了401、409还是422也会漏掉具体错误体。先检查约定的响应再决定是否需要把意外状态转换为异常定位信息会更完整。七、一次真实的错误预期401还是422最初很容易把“缺少密码”理解为登录失败并把预期状态码写成401response api_client.session.post( f{api_client.base_url}/api/login, json{username: api_user}, timeoutapi_client.timeout, ) ​ assert response.status_code 401单独运行后Pytest给出的结果是E assert 422 401 E where 422 Response [422].status_code ​ FAILED examples/test_wrong_status_expectation.py 1 failed in 0.73s原因是请求体连password字段都没有FastAPI先执行请求模型校验在进入账号认证逻辑之前就返回422。只有请求结构完整、用户名或密码内容不正确时才进入登录逻辑并返回401。因此修复方式不是把接口强行改成401而是先确认接口契约如果约定由框架统一处理字段缺失用例就应该断言422并继续检查错误位置是[body, password]、错误类型是missing。红色结果只说明实际结果和测试预期不一致最终修改哪一边要依据接口约定判断。八、退出登录后要继续使用原Token退出接口返回204只能证明请求被接受不能证明Token真的失效。本文在同一个Session中退出再用原请求头访问资料接口def test_logout_revokes_the_current_token( authenticated_client: AuthApiClient, ) - None: logout_response authenticated_client.logout() profile_response authenticated_client.profile() ​ assert logout_response.status_code 204 assert logout_response.content b assert profile_response.status_code 401 assert profile_response.json()[detail][code] TOKEN_INVALID这里刻意没有删除Session中的Authorization头因为验证目标就是确认服务端已撤销Token。若客户端先清空请求头再访问得到401只能证明“没有Token不能访问”无法证明原Token已经失效。九、用慢响应验证客户端超时本地服务提供一个延迟200毫秒返回的接口测试把连接超时设为100毫秒、读取超时设为50毫秒with pytest.raises(requests.Timeout): api_client.get( /api/slow, params{delay_ms: 200}, timeout(0.1, 0.05), )二元组中的第一个值控制建立连接第二个值控制等待响应数据。本次服务运行在本机连接很快建立随后因为读取阶段超过50毫秒而抛出requests.Timeout。需要注意Requests的读取超时不是整个响应下载的绝对总时长而是底层连接在指定时间内没有收到数据时触发。生产项目中的超时值应根据服务目标、网络环境和重试策略确定本文使用较短时间只是为了稳定复现超时路径。十、几个常见问题1. 所有接口都只断言状态码同样返回200响应可能缺字段、字段类型错误或Token不可用。成功接口至少检查关键响应字段并在可能时继续执行一次依赖该结果的请求。2. 多条用例共用固定账号固定账号在并发、重复运行和失败重试时容易发生状态冲突。可以给测试数据增加唯一后缀并在fixture中建立与清理。如果连接真实数据库还要准备定期回收机制处理进程异常退出留下的数据。3. 把Token写进代码或配置文件本文Token由接口动态生成输出样例也已经脱敏。真实Token、Cookie和账号凭据不能提交到Git仓库也不应该直接出现在截图和日志中。4. 负向用例只判断抛出了异常raise_for_status()适合业务代码快速阻止错误响应继续传播但接口契约测试应该明确检查状态码、响应头和错误体否则不同错误可能被压缩成同一种HTTPError。5. 为了让用例通过而放宽超时超时偶发不一定意味着阈值太小也可能是服务变慢、连接未释放或运行环境异常。调整数值前应先区分连接超时和读取超时再结合接口耗时分布判断。十一、小结与思考Pytest和Requests组合起来并不复杂真正影响接口测试质量的是用例是否覆盖了完整状态链路。本文从注册开始验证登录生成Token、Token访问资料、退出撤销Token以及慢响应触发超时正常测试集共收集8条全部通过。接口断言也需要分层状态码说明结果类别响应体承载字段和业务错误码响应头体现协议约定后续请求确认状态变化超时处理负责不可用路径。把测试数据创建与清理放进fixture并让每条测试拥有独立账号回归次数增加后仍能保持可重复运行。本文没有覆盖Token真实签名、过期刷新、权限角色、并发登录、限流和数据库事务。这些属于更完整鉴权系统的验证范围可以在当前请求客户端和fixture结构上继续扩展但不能从当前8条用例推导整个登录系统已经得到完整覆盖。参考资料Requests QuickstartRequests Advanced UsageFastAPI Security First StepsFastAPI Security Tools本文代码GitHub005-api-testing-with-pytest