尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

iHRM登录接口封装实战:从Token管理到接口测试自动化

iHRM登录接口封装实战:从Token管理到接口测试自动化 做接口测试这些年我最大的感受是真正难的不是调通单个接口而是把一堆互相依赖的接口串起来稳定跑起来。iHRM这种人力资源管理系统就很典型——你想测员工管理、考勤打卡、薪资核算每一个模块都绕不开登录。所以当我梳理这个项目的接口自动化方案时第一件事不是写业务用例而是先把登录接口完整封装好。这篇文章就以iHRM登录接口为例讲清楚封装的设计思路、核心代码、工具配合以及那些只有真跑过才会遇到的坑。如果你正准备做接口测试自动化或者被“每个脚本都要先登录”这种重复劳动烦到了这篇文章可以直接给你一套能落地的方案。所谓封装不是把一条登录请求包成函数就完事。它要把认证逻辑、请求会话、环境配置、日志记录这些细节全部收敛到一起让后续几十个业务接口用例只需要关心自己的业务参数不用再重复处理token、不用关心环境地址、不用每次手动把请求头拼一遍。这篇分享我会从设计思路一路讲到Python代码实现再补充Postman和JMeter里的对比玩法内容偏向实际工程不是那种只贴两行代码的教程。1. 项目实战前的整体设计与思路拆解1.1 登录接口在iHRM项目里到底是什么角色iHRM是一个典型的前后端分离管理系统前端负责页面交互后端提供RESTful接口。用户打开登录页输入手机号和密码前端把请求发到登录接口服务端校验通过后返回一个token前端把这个token存下来之后所有业务接口在请求头里带上这个token服务端才能识别“你是你”。对测试来说这意味着一个很朴素的现实没有token除了登录接口本身其他接口全都不可用。可能有人觉得登录接口太简单一个POST请求而已。但恰恰是这种“所有接口都依赖它”的接口才最值得花精力好好封装。它不是项目中技术含量最高的部分却是整个接口自动化测试工程的地基。地基不稳后面用例写得再漂亮也白搭。我在做这个项目时第一版脚本就是最原始的写法每个用例文件里都写一遍登录请求把返回的token复制出来再拼到下一个请求的头里。结果接口一调整几十个文件全部要跟着改光是排查“为什么token取不到”就浪费了大量时间。后来痛定思痛才决定把登录收敛成一个统一模块。1.2 封装要解决的实际问题把登录接口封装起来不是为了让代码看起来“高级”而是要解决几个非常具体的痛点。第一个是代码重复。不封装每个脚本里都要出现登录请求、token提取、请求头拼接这套逻辑少说七八行多则十几行。十个用例就是一百行重复代码维护成本肉眼可见地涨。第二个是环境切换困难。开发环境、测试环境、演示环境的接口地址不一样账号密码也可能不一样。如果这些信息散落在脚本里每次切换环境都要挨个改文件改漏一个就等着报错吧。封装之后环境配置收敛到配置文件里换环境就是换一个配置的事。第三个是token管理混乱。有人用全局变量有人写死有人每次用例跑完不清理导致测试之间互相影响。封装之后登录态的生命周期由统一代码管理该登录就登录该复用就复用该失效处理就失效处理。第四个是可读性差。业务用例里堆满登录请求和token处理逻辑读代码的人根本看不清你到底在测什么。封装之后业务用例里一句client.login()然后直接写业务接口的请求和断言逻辑一目了然。1.3 为什么我选择用Python requests做核心封装接口测试的工具很多Postman、JMeter、Apifox、Python、Java都有各自的生态。我最终选择在Python requests库上做核心封装不是因为它功能最多而是因为它最适合“测试工程化”这条路线。Postman适合做接口调试和临时验证界面直观collection还能分享给团队。但它做自动化有几个硬伤流程控制能力弱、断言表达力有限、生成测试报告不方便、难以跟CI/CD流水线集成。把它当“瑞士军刀”用可以当“自动化基础设施”用就吃力了。JMeter适合做性能测试和负载测试模拟多用户并发是它的强项。但它的脚本本质上是XML版本对比难逻辑复杂以后维护体验一言难尽。我一般只在需要压测的时候才用它功能测试还是交给代码。Python requests加上pytest框架能写灵活的断言、能数据驱动、能接入CI、能生成Allure报告几乎覆盖了功能测试自动化所有需求。还有很多人提到axios二次封装那是前端开发里的概念思路和测试侧相通——都是把重复的请求逻辑收拢起来但技术栈不同这里是后端/测试侧用Python来解决。2. 登录接口底层逻辑与关键参数拆解2.1 登录请求的报文结构封装的第一个前提是把登录接口的报文结构彻底吃透。以我在iHRM项目里实际遇到的接口为例它通常是这样设计的请求方式POST请求路径/api/sys/login请求头Content-Type: application/json请求体{ mobile: 13800000002, password: 123456 }成功时的响应体{ success: true, code: 10000, message: 操作成功, data: { token: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMzgwMDAwMDAwMiJ9.xxxxx } }这里有几个关键字段需要关注。success表示业务是否成功code是业务状态码message是提示信息data.token才是我们真正要提取的东西。封装的时候不能只看HTTP状态码很多测试新手习惯用resp.status_code 200判断登录成功这在iHRM这类接口上是不够的——HTTP是200业务可能是失败比如密码错误时也会返回200但success是false。我在封装时定了一条原则以业务状态码和success字段为准HTTP状态码只作为传输层参考。这样接口后续如果调整了返回结构至少不会因为HTTP层的正常返回而漏报业务错误。2.2 Token机制为什么不用Cookie而用TokeniHRM这类前后端分离项目基本都走Token认证而不是传统的Session-Cookie。原因其实很好理解前端和后端可能部署在不同的域名下Cookie跨域处理很麻烦而Token放在请求头里不受Cookie域名的限制。具体来说服务端拿到用户名密码后会生成一段JWTJSON Web Token返回给前端。JWT由三部分组成Header头部、Payload载荷、Signature签名用点号分隔。Payload里通常会包含用户ID、过期时间等关键信息最后一段签名保证Token内容没有被篡改。前端拿到Token后在后续请求的请求头里加一行Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxxxx服务端收到请求后解析并验证签名确认无误后就知道当前请求是哪个用户发起的。整个过程服务端不需要保存会话状态所以叫“无状态认证”。对测试来说理解这个机制的意义在于当我们说“保持登录态”时本质就是“把Token正确传递到后续请求中”。封装登录接口后我需要让这个Token自动出现在所有业务请求的请求头里而不是每个用例手动写一遍。这也是封装的核心价值之一。2.3 登录接口那些容易被忽略的细节真正把登录接口封装好光看请求参数还不够有几个细节我在实际测试中踩过坑这里重点提一下。第一个是密码加密。有些系统前端会先把密码做一次MD5或者RSA加密再提交如果封装时忽略了这一点用明文密码直连接口就会一直登录失败。iHRM的教学环境通常不加密但真实项目一定要确认清楚最好的办法是抓一次浏览器发出的实际请求看看请求体里的密码是明文还是密文。第二个是验证码。部分环境会开启验证码校验登录接口多出captcha参数。如果测试环境开了这个开关自动化脚本会很痛苦。我通常会在环境配置里留一个开关或者跟开发确认测试环境是否需要关闭验证码而不是在代码里写一堆识别逻辑。第三个是账号锁定策略。同一个账号连续输错密码系统可能会锁定一段时间。自动化测试跑久了如果账号被锁登录封装再漂亮也没用。建议准备几个测试账号轮流用或者用完后主动把密码改回标准状态。第四个是Token有效期。登录返回的Token不是永久的有的系统有效期设2小时有的设24小时。测试脚本如果长时间挂着不重新登录中途可能突然遇上一波401报错。这个问题在第4章会单独展开讲。3. 实操过程与核心环节实现3.1 工程目录与依赖准备封装登录接口之前我先梳理了一下整个自动化项目的工程结构。不管项目规模大小我建议至少把“配置”、“公共封装”、“测试用例”、“报告”这四类内容分开目录结构大致如下ihrm_api_test/ ├── config/ │ ├── dev.ini │ ├── test.ini │ └── prod.ini ├── common/ │ ├── __init__.py │ └── client.py ├── testcases/ │ ├── __init__.py │ ├── test_employee.py │ └── test_attendance.py ├── reports/ └── requirements.txt依赖方面核心只需要requests和pytest如果要做数据驱动再加一个pytest-datadir之类的插件不需要额外引入太重的东西。安装命令很简单pip install requests pytest pytest-html我见过有人一上来就搞一套复杂框架结果光依赖问题就折腾了半天。接口自动化的起步阶段保持简单是最重要的后面有需要再逐步加。3.2 配置文件把环境差异挡在代码之外配置文件的定位很简单任何会因为环境不同而变化的参数都放进配置里不要写死在代码中。以一个test.ini为例[server] base_url http://ihrm-java.itheima.net [account] mobile 13800000002 password 123456 [timeout] login_timeout 10 [log] level INFO这里base_url是接口服务的基础地址account是测试账号信息。如果要在开发环境跑新建一个dev.ini把base_url换成开发地址就行代码完全不用动。有同学可能会问账号密码放在配置文件里会不会不安全测试环境的账号本来就是测试专用的风险可控。但要注意两点生产环境的任何信息都不要放进来以及配置文件不要提交到公开仓库。我在.gitignore里会把config/prod.ini直接忽略掉。3.3 核心代码实现从登录到统一请求封装配置好了之后开始写核心的封装类。我给它起名叫IHRMClient一个类负责整个iHRM系统的接口交互。这个类的核心职责有两个一是登录并管理Token二是提供统一的get、post方法让业务用例不用关心认证细节。import logging import configparser import requests class IHRMClient: def __init__(self, envtest): self.cfg configparser.ConfigParser() self.cfg.read(fconfig/{env}.ini) self.base_url self.cfg.get(server, base_url) self.session requests.Session() self.token None self.logger logging.getLogger(ihrm) logging.basicConfig( levelself.cfg.get(log, level), format%(asctime)s %(levelname)s %(message)s, ) def login(self, mobileNone, passwordNone): mobile mobile or self.cfg.get(account, mobile) password password or self.cfg.get(account, password) url f{self.base_url}/api/sys/login payload {mobile: mobile, password: password} headers {Content-Type: application/json, Accept: application/json} self.logger.info(f登录请求 - POST {url}) resp self.session.post(url, jsonpayload, headersheaders, timeout10) self.logger.info(f登录响应 - {resp.status_code}) if resp.status_code ! 200: raise RuntimeError(f登录接口HTTP异常: {resp.status_code} {resp.text}) data resp.json() if data.get(success) and data.get(data): self.token data[data][token] self.session.headers.update({Authorization: fBearer {self.token}}) self.logger.info(登录成功Token已获取) return self.token raise RuntimeError(f登录业务失败: code{data.get(code)} message{data.get(message)}) def get(self, path, **kwargs): return self.request(GET, path, **kwargs) def post(self, path, **kwargs): return self.request(POST, path, **kwargs) def request(self, method, path, **kwargs): if not self.token: self.logger.info(检测到未登录先自动登录再请求业务接口) self.login() url self.base_url path if path.startswith(/) else f{self.base_url}/{path} self.logger.info(f业务请求 - {method} {url}) resp self.session.request(method, url, timeout10, **kwargs) self.logger.info(f业务响应 - {resp.status_code} {resp.text[:200]}) return resp这段代码有几个设计点值得展开说。第一登录接口的请求头单独设置。因为requests的Session对象在第一次请求前还没有Authorization头登录接口本身也不需要认证所以登录时显式传入请求头避免意外带上脏数据。第二登录成功后把Token更新到Session对象的默认请求头里。这样后续所有通过client.get或client.post发出去的请求都会自动带上Authorization: Bearer xxx业务用例里完全不用管Token这件事。第三request方法里做了“未登录自动登录”的兜底。有时候用例执行顺序调整或者前一个用例把Token搞过期了直接调业务接口会得到401。有了这个兜底逻辑只要Client实例还在就能自动重新登录后继续请求大大提升测试稳定性。第四日志中只打印响应前200个字符。这是考虑到有些接口返回体很大全量打日志会刷屏而且敏感字段可能被泄露到日志文件里。截断后兼顾了可读性和安全性。3.4 进阶技巧自动判断Token是否过期如果Token有效期比较短或者测试时间跨度大可以进一步封装一个“自动重新登录”的机制。实现的思路不复杂自定义一个handle_401逻辑当业务请求返回401时强制清空Token并重新登录然后重放一次原请求。def request_with_retry(self, method, path, retries1, **kwargs): resp self.request(method, path, **kwargs) if resp.status_code 401 and retries 0: self.logger.warning(收到401Token可能已过期重新登录后重试一次) self.token None self.login() resp self.request(method, path, **kwargs) return resp这个逻辑能解决大部分Token过期导致的测试失败。但要注意如果服务端关闭了旧Token、销毁了原来的会话重试时就要使用同一个Session对象因为里面的Cookie和Header状态是连续的。上面封装里更新的是同一个self.session所以重试是可靠的。3.5 配合pytest使用登录态只建立一次当测试用例多起来以后如果每个用例都实例化一个新的IHRMClient并登录一次整体执行时间会变得很长。我的做法是使用pytest的session级fixture让整个测试会话只登录一次然后让业务用例共享这个client实例。import pytest from common.client import IHRMClient pytest.fixture(scopesession) def client(): client IHRMClient(envtest) client.login() return client def test_create_employee(client): resp client.post(/api/sys/user, json{ username: 测试员工001, mobile: 13800000003, timeOfEntry: 2024-01-01 }) assert resp.status_code 200 result resp.json() assert result.get(success) is True这样一来整个测试执行过程中登录只发生一次。业务用例之间通过共享的client持有Token效率高代码也干净。当然如果业务用例之间需要完全隔离的数据状态可以调低fixture的scope改成function级代价是每个用例都要登录一次。这需要根据项目实际情况权衡。4. 常见问题与排查技巧实录4.1 登录接口失败的速查手册我把实际运行中经常遇到的登录失败场景整理成了一张表供大家参考现象可能原因排查方向HTTP 401 Unauthorized账号密码错误或Token失效检查账号配置抓包对比请求体HTTP 200但successfalse业务校验未通过看code和message字段通常是验证码或账号锁定请求超时网络不通或服务未启动先用浏览器访问base_url确认服务可用密码错误明文和密文混用抓包查看真实请求参数Token取不到响应结构理解错误打印完整响应体确认data.token是否存在偶发500服务端逻辑异常让开发看日志关注是否频繁登录触发限流实际排查时一个很有效的手段是拿Postman先手调一遍确认接口本身没问题再对比脚本里的配置和参数。很多时候问题不在代码而在配置里的base_url少写了一个斜杠或者账号密码从Excel复制过来多了个空格。4.2 用Postman调试iHRM登录接口的做法虽然Python封装是主线但Postman在调试阶段仍然不可替代。我调试登录接口的流程是这样。先在Postman里新建一个Collection名字叫iHRM然后添加一个登录请求。URL填{{base_url}}/api/sys/login这里base_url是在Environment管理里配置的变量。Body用raw JSON格式填入手机号和密码。请求成功后在Tests标签里写一段脚本把Token存到环境变量里const jsonData pm.response.json(); if (jsonData.success jsonData.data) { pm.environment.set(token, jsonData.data.token); }然后给业务接口的请求头设置Authorization: Bearer {{token}}Postman会自动从环境变量里取Token。这里有个坑环境变量是有作用域的如果Token存错了作用域比如存到了全局变量而请求读取的是环境变量就会出现“明明登录成功了但业务接口还是401”的诡异问题排查的时候先看一眼Variables面板里的值是否真的写进去了。4.3 JMeter登录后开5个线程跑查询接口怎么做热搜词里有一条很典型“jmeter 模拟登录后同时跑5个线程跑查询接口”。这个需求本质上是在解决“登录一次多线程并发查询”的问题。JMeter里的做法有两种适用场景完全不同。第一种是每个线程独立登录。在线程组里同时放登录请求和查询请求线程数设置5这样每个线程都会先执行登录再执行查询。好处是贴近真实用户行为坏处是登录请求也被并发执行如果系统对登录接口有限流可能会触发风控。第二种是只登录一次所有线程共享Token。做法是利用JMeter的setUp线程组把登录请求放在专门负责初始化的setUp线程组里通过JSON提取器提取Token再用__setProperty函数把Token存成JMeter全局属性。业务线程组里的请求通过${__P(token)}读取这个全局属性拼到请求头里。具体步骤记录一下添加setUp线程组线程数设为1。在setUp线程组里添加HTTP请求配置登录接口的URL、请求体和Header。在登录请求下面添加JSON提取器变量名填accessTokenJSON表达式填$.data.token。添加一个BeanShell PostProcessor写入${__setProperty(token, ${accessToken},)}把Token提升为全局属性。添加普通线程组线程数5循环次数自己定。在普通线程组里添加HTTP请求路径填查询接口。添加HTTP Header Manager增加一个HeaderAuthorization值填Bearer ${__P(token)}。这样做的好处是登录只发生一次5个线程都用同一个Token并发查询避免了登录逻辑干扰查询接口的压测结果。如果你的业务场景就是“每个用户独立登录再查询”那就用第一种方案各自独立登录再查询更接近真实场景。两种方案没有优劣关键看测试目标。4.4 Python多线程下的登录态共享问题Python的requests.Session在官方文档里并没有承诺线程安全。也就是说多个线程共享同一个Session实例并发发请求存在潜在风险比如Header被覆盖、连接池异常等。我通常在Python里跑并发测试时会使用threading.local()为每个线程创建一个独立的Session实例并各自执行登录。这样虽然多登录了几次但能保证线程隔离避免出现神秘的偶发失败。import threading from common.client import IHRMClient thread_local threading.local() def get_client(): if not hasattr(thread_local, client): thread_local.client IHRMClient(envtest) thread_local.client.login() return thread_local.client每个线程都拿到自己的登录态互相之间不干扰。如果你确实要求“所有线程共用一个Token”可以改成把Client实例设为全局共享但只要有一个线程触发重新登录其他线程手上的Token就全作废了这种设计的容错性很差我一般不建议。5. 把登录封装融入完整接口自动化体系5.1 登录封装之后业务用例能简洁到什么程度不封装的时候一个“创建员工”用例可能要写二十多行登录、提取Token、拼Header、发起业务请求、断言。封装之后同样的用例压缩到几行import pytest from common.client import IHRMClient pytest.fixture(scopesession) def client(): return IHRMClient(envtest).login() def test_query_employee(client): resp client.get(/api/sys/user/1) assert resp.status_code 200 body resp.json() assert body[success] is True注意这里IHRMClient(envtest).login()返回的是Token字符串如果继续链式调用fixture拿到的是token而不是client不符合我们的预期。所以实际编码时我会在login方法末尾返回self或者单独写一个工厂函数。这个细节很考验封装设计我当时就因为返回值搞错过一次最后统一规定login()返回client本身def login(self, mobileNone, passwordNone): # 登录逻辑 ... return self这样client IHRMClient(envtest).login()拿到的就是能直接发起业务请求的client对象。5.2 前后端分离项目里的另一条线axios二次封装热搜里有“axios二次封装”这个词虽然它是前端开发的方向但和测试侧封装登录接口的思路高度相似。前端在做iHRM这类项目时通常会在axios基础上封装一个统一的请求模块统一配置baseURL、请求拦截器、响应拦截器、Token携带逻辑遇到401还能统一跳转登录页。测试侧的IHRMClient和前端的axios封装本质上是同一件事的两个镜像都是在“请求发起前统一注入认证信息”在“响应回来后统一处理错误和Token刷新”。理解了这一层你会发现不管用哪个语言、哪个框架封装的核心骨架是一致的。这也是为什么我建议测试人员不要只停留在工具阶段稍微看看前端代码很多设计思路是通用的。5.3 接口测试面试里关于登录的高频追问登录接口太常见了面试官很喜欢拿它来考察候选人的基础功底。我梳理了几个高频追问其实在前面封装过程中都能找到答案。第一个问题登录接口的测试用例怎么设计回答思路是分维度正常场景、异常密码、手机号格式错误、用户不存在、账号锁定、验证码错误、Token过期、并发登录、密码传输加密等。每个维度都要落到具体的请求参数和预期结果上。第二个问题自动化测试里怎么保持登录态回答思路是调用登录接口获取Token把Token通过HeaderManager、环境变量、统一Client等方式传递到后续请求。Python里就是Session对象的Header注入Postman里是环境变量JMeter里是全局属性。第三个问题Token过期了怎么办回答思路有两种一种是在用例层判断401后重新登录并重试另一种是提前解析Token里的过期时间在Token快过期时主动重新登录。两种方案各有优劣第一种实现简单但重试有一次失败成本第二种更优雅但需要服务端开放Token解析能力。第四个问题如何保证多个测试用例之间的登录态不互相干扰回答思路是通过fixture管理共享的client实例或者利用线程隔离为每个线程创建独立的登录态。重点是把“登录”从业务用例中抽离出来避免每个用例都去处理登录细节。这些问题在面试中其实没有标准答案但如果你能拿出“我封装了一个IHRMClient登录后自动注入Header遇到401自动重试”这样的实际项目经历会比背概念有说服力得多。这次封装iHRM登录接口的实践我最大的体会是封装不是把代码写得花里胡哨而是把重复劳动的复杂度集中管理把容易出错的地方收口到一个可靠模块里。后来我把同一套思路迁移到其他系统的接口自动化上改的基本只有配置文件和接口路径。如果你也在做接口自动化不妨先从封装登录接口下手——它看起来不起眼却是整个项目最值得先投入的地方。
返回列表