目录摘要1. 前后端分离后接口是唯一的契约2. 能发现 UI 测试发现不了的问题3. 测试左移更早发现问题4. 自动化后可重复执行回归测试利器一、接口测试用例设计---思维导图编辑二、本次测试所需要的工具以及环境1、开发环境2、Python 第三方库3、Python 标准库4、测试报告工具三、搭建接口测试框架1.测试架构2.编写代码1封装工具类封装日志类 封装请求类封装yaml类2用例编写登录接口列表页接口详情页接口用户认证接口编辑页接口用户信息接口执行测试用例指定测试用例执行顺序生成测试报告并分析结果本次测试的gitee仓库https://gitee.com/sddsfsdf455/blog_-api_-auto-test摘要接口测试是测试系统各组件之间的接口API是否符合预期是软件测试里性价比最高的一层。------比单元测试覆盖范围大能测模块间交互比 UI 测试快且稳定不依赖页面元素1. 前后端分离后接口是唯一的契约现在的系统基本都是前后端分离前端页面调用后端接口拿数据后端接口返回 JSON 给前端渲染接口就是前后端之间的合同如果接口返回的数据格式不对、字段缺失、类型错误前端页面直接崩。接口测试就是提前验证这个 合同 有没有履行。2. 能发现 UI 测试发现不了的问题很多问题在页面上看不出来但接口层面已经暴露了越权漏洞A 用户能看 B 用户的数据本次测试参数校验缺失传非法值字符串、特殊字符、空值接口不报错直接 500数据泄露接口返回了不该返回的敏感字段密码、手机号并发 / 性能问题接口响应慢、高并发下报错状态码不规范业务失败也返回 200前端无法判断这些问题点页面可能 看起来正常但接口层面已经有严重隐患3. 测试左移更早发现问题单元测试开发写完函数就能测接口测试后端接口开发完、前端页面还没写好就能测UI 测试必须等前端页面全部开发完才能测接口测试可以在项目早期就介入不用等页面做好。越早发现 bug修复成本越低—— 需求阶段改只要 1 小时上线后改可能要几天。4. 自动化后可重复执行回归测试利器接口一旦写好变化频率比 UI 低得多。以后每次代码更新、版本发布跑一遍就知道有没有把老功能搞坏回归测试。如果靠人工点页面测费时费力还容易漏。接口测试是用最低的成本最早、最稳定地发现系统模块间交互问题的测试手段。 前后端分离的项目里接口是系统的骨架骨架稳了页面才不会出大问题。一、接口测试用例设计---思维导图二、本次测试所需要的工具以及环境1、开发环境操作系统Windows编程语言Python 3.10开发工具PyCharm Community Edition 2022.1.3以及postman虚拟环境venv项目内置用于隔离依赖2、Python 第三方库pytest测试框架负责收集和运行测试用例requests发送 HTTP 请求支持 GET、POST 等请求方式PyYAML读写 YAML 格式的测试数据文件jsonschema对接口返回的 JSON 数据进行结构校验allure-pytest生成 allure 测试结果数据pytest-order控制测试用例的执行顺序通过 pytest.mark.order 装饰器实现通过在控制台输入pip install ......来导入对应的包3、Python 标准库logging日志记录os文件路径与目录操作time时间格式化用于生成带日期的日志文件名base64图片转 base64 编码如需传图片参数4、测试报告工具Java 运行环境JDKallure 命令行工具的运行依赖需配置 JAVA_HOME 环境变量allure 命令行工具将测试结果数据转换为可视化的 HTML 报告可通过 scoop install allure 安装或手动下载后配置 Path 环境变量三、搭建接口测试框架在虚拟环境下编写代码 避免污染全局环境1.测试架构pytest.ini 的内容为addopts -vs --alluredir allure-results2.编写代码1封装工具类封装日志类import logging import os.path import time class info_filter(logging.Filter): def filter(self, record): return record.levelno logging.INFO class err_filter(logging.Filter): def filter(self, record): return record.levelno logging.ERROR class logger: # 使用类方法 为类新增一个类方法 classmethod def getlog(cls): # 获取日志记录器对象 指向Logger 名称为root cls.my_logger logging.getLogger() # 定义日志级别最低为DEBUG cls.my_logger.setLevel(levellogging.DEBUG) logs 2026-8-27.log 2026-8-27-info.log 2026-8-27-err.log # 创建logs文件夹将日志文件储存进去 方便查看 LOG_PATH ./logs/ if not os.path.exists(LOG_PATH): os.mkdir(LOG_PATH) now time.strftime(%Y-%m-%d) log_name LOG_PATH now .log info_log_name LOG_PATH now -info.log err_log_name LOG_PATH now -err.log # 创建一个日志文件处理器 将日志信息填入指定文件中 # 创建⼀个 FileHandler 对象指定⽇志⽂件的名称为 log_name # 这个处理器会将⽇志信息写⼊到指定的⽂件中 all_handler logging.FileHandler(filenamelog_name, encodingutf-8) info_handler logging.FileHandler(filenameinfo_log_name, encodingutf-8) err_handler logging.FileHandler(filenameerr_log_name, encodingutf-8) # 创建⼀个⽇志格式器对象 formatter logging.Formatter( %(asctime)s %(levelname)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d)] - %(message)s ) # 将格式器设置到处理器上 all_handler.setFormatter(formatter) info_handler.setFormatter(formatter) err_handler.setFormatter(formatter) # 设置文件过滤器 info_handler.addFilter(info_filter()) err_handler.addFilter(err_filter()) # 添加处理器到记录器当中 # 这样日志记录器就会使用处理器处理日志信息 cls.my_logger.addHandler(all_handler) cls.my_logger.addHandler(info_handler) cls.my_logger.addHandler(err_handler) return cls.my_logger自定义 logger 日志封装类通过getlog类方法完成日志记录器的统一配置设置日志级别为 DEBUG自动创建 logs 目录并按日期生成总日志、INFO 日志、ERROR 日志三个文件创建三个 FileHandler 分别写入对应文件配置统一的 Formatter 格式含时间、级别、文件名、函数名、行号、消息内容通过自定义 info_filter 和 err_filter 过滤器实现分级输出info.log 仅存 INFO 级别、err.log 仅存 ERROR 级别。最终返回配置完成的 logger 实例调用方直接使用即可无需重复配置实现日志功能的统一封装与复用。设置日志过滤器 将其放在日志处理器上把对应级别的日志放入专门设置的对应的日志文件封装请求类import requests from utils.logging_utils import logger host http://49.235.61.184:19090/ class Request: log logger.getlog() def get(self, url, **kwargs): self.log.info(准备发起GET请求urlurl) self.log.info(接口信息{}.format(kwargs)) r requests.get(urlurl, **kwargs) self.log.info(接口响应状态码{}.format(r.status_code)) self.log.info(接口的响应数据是{}.format(r.text)) return r def post(self, url, **kwargs): self.log.info(准备发起POST请求url url) self.log.info(接口信息{}.format(kwargs)) r requests.post(urlurl, **kwargs) self.log.info(接口响应状态码{}.format(r.status_code)) self.log.info(接口的响应数据是{}.format(r.text)) return r自定义一个 Request 请求类在类内封装 get 和 post 请求方法接收 url 及其他请求参数。该方法在调用原生 requests.get/requests.post 发送请求的前后通过 logging 日志记录器分别记录请求信息URL、请求参数和响应信息状态码、响应数据实现了在不修改原生 requests 方法的前提下为请求过程增加统一的日志记录功能。日志可同时输出到控制台和日志文件便于排查问题。封装yaml类 使用yaml import os import yaml # 在yaml文件中存储数据 def write_yml(filename, data): with open(os.getcwd() /data/ filename, modea, encodingutf-8) as f: yaml.safe_dump(data, streamf) # 在yaml文件中读取数据并返回需要的信息-token登录凭证 def read_yml(filename, key): with open(os.getcwd() /data/ filename, moder, encodingutf-8) as f: data yaml.safe_load(streamf) return data[key] # 清理yaml中的数据 def clear_yml(filename): with open(os.getcwd() /data/ filename, modew, encodingutf-8) as f: f.truncate()自定义 YAML 数据读写工具封装write_yml、read_yml、clear_yml三个函数实现测试数据的统一管理write_yml以追加模式打开文件通过yaml.safe_dump将接口返回的动态数据如 token、blogId写入 YAML 文件实现跨用例数据传递read_yml以只读模式打开文件通过yaml.safe_load解析内容并按 key 返回对应数据值供测试用例读取使用clear_yml通过truncate清空文件内容用于测试前重置数据避免历史数据干扰。三个函数统一一起使用os.getcwd()拼接 data 目录路径实现数据与代码分离便于维护和扩展。2用例编写登录接口 登录-接口自动化测试 url--... 请求数据为json格式{userName:zhangsan,password:123456} 利用jsonSchema校验接口返回数据 保存登陆凭证数据 import re import pytest from jsonschema.validators import validate from utils.request_utils import host, Request from utils.yaml import write_yml pytest.mark.order(1) class TestLogin: url host user/login schema { type: object, additionalProperties: False, properties: { code: { type: number }, errMsg: { type: [null, string] }, data: { type: [object, null], properties: { userId: { type: number }, token: { type: string } }, required: [ userId, token ] } }, required: [ code, errMsg, data ] } pytest.mark.parametrize(login, [ { userName: *****, password: ********, }, { userName: ********, password: ********, } ]) def test_login_success(self, login): json { userName: login[userName], password: login[password] } r Request().post(urlself.url, jsonjson) validate(instancer.json(), schemaself.schema) assert r.json()[code] 200 assert re.match(r\S{100,}, r.json()[data][token]) # 保存用户的登录凭证token data { user_token: r.json()[data][token] } write_yml(data.yaml, data) pytest.mark.parametrize(login, [ { userName: *****, password: ***, }, { userName: ********, password: *******, }, { userName: ******, password: ******, }, { userName: , password: ****, }, { userName: ********, password: , }, { userName: , password: , }, { userName: ******, password: *******1111, }, { userName: *********11111111111111111, password: *********, } ]) def test_login_fail(self, login): json { userName: login[userName], password: login[password] } r Request().post(urlself.url, jsonjson) validate(instancer.json(), schemaself.schema) assert r.json()[code] -1 assert r.json()[data] is None定义一个登录测试类------未登录状态下访问测试用例登录状态下访问的测试用例并分为正常测试用例和异常测试用例使用pytest中内置的pytest.mark.parametrize装饰器对测试函数的参数进⾏参数化。以达到测试多组数据的目的获取到登录接口的请求数据之后 将它写入yaml文件中并保存起来以便之后调用所需数据JSON Schema⼀个⽤来定义和校验JSON的web规范简⽽⾔之JSON Schema是⽤来校验json是否符合预期。根据json创建 JSON Schema 后在类中定义一个schema并 将得出的数据放入其中validate方法可以将识别得到的数据类型和请求的url的请求信息进行匹配验证请求信息是否出现错误如何获取测试用例的json数据1.打开postman 输入正确的网址以及添加所需的请求数据比如复制整段json数据 并且打开https://json.tqji.cn/to-schema这样就可以获取相应的Schema数据了最后使用validate方法将请求到的的网页数据和取得的Schema数据比较来验证数据类型是否匹配最后使用assert 进行断言具体的数据内容是否符合预期。列表页接口import pytest from jsonschema.validators import validate from utils.request_utils import host, Request from utils.yaml import read_yml, write_yml pytest.mark.order(2) class TestList: url host blog/getListByPage Schema { type: object, additionalProperties: False, properties: { code: { type: number }, errMsg: { type: null }, data: { type: object, properties: { total: { type: number }, pages: { type: number }, pageNum: { type: number }, pageSize: { type: number }, records: { type: array, items: { type: object, properties: { id: { type: number }, title: { type: string }, content: { type: string }, createTime: { type: string } }, required: [ id, title, content, createTime ] } } }, required: [ total, pages, pageNum, pageSize, records ] } }, required: [ code, errMsg, data ] } def test_List_No_login(self): r Request().get(urlself.url) assert r.status_code 401 def test_List_login(self): token read_yml(data.yaml, user_token) header { User_token: token } r Request().get(urlself.url, headersheader) validate(instancer.json(), schemaself.Schema) # 获取列表页的具体blogID data { blogId: r.json()[data][records][0][id] } write_yml(data.yaml, data)列表接口的测试方式也和登录接口的测试步骤差不多包括未登录状态下访问和登录状态下访问。获取后续接口测试所需要的blogId数据调用write_yaml方法将在列表页接口的数据中获得的blogId存入yaml文件中方便后续测试详情页接口import pytest from jsonschema.validators import validate from utils.request_utils import host, Request from utils.yaml import read_yml class TestDetail: url host blog/getBlogDetail? Schema { type: object, properties: { code: { type: number }, errMsg: { type: [null, string] }, data: { type: [object, null], properties: { id: { type: [number, null] }, title: { type: string }, content: { type: string }, userId: { type: number }, createTime: { type: string } }, required: [ id, title, content, userId, createTime ] } }, required: [ code, errMsg, data ] } # 未登录状态下访问详情页接口 def test_detail_no_login(self): r Request().get(urlself.url blogId str(123333)) assert r.status_code 401 # 登录状态下访问 def test_detail_login_success(self): BlogId read_yml(data.yaml, blogId) token read_yml(data.yaml, user_token) header { User_token: token } r Request().get(urlself.url blogId str(BlogId), headersheader) validate(instancer.json(), schemaself.Schema) assert r.json()[code] 200 assert r.json()[errMsg] is None pytest.mark.parametrize(blog_id, [ { blogId: 666666, errMsg: Source must not be null }, { blogId: , errMsg: 参数校验失败 }, { blogId: -1, errMsg: Source must not be null }, { blogId: 0, errMsg: Source must not be null }, { blogId: qq1!!!, errMsg: Method parameter blogId: Failed to convert value of type java.lang.String to required type java.lang.Integer; For input string: \qq1!!!\ }, { blogId: 222, errMsg: Source must not be null }, { blogId: 222222222222222222222222222222222222222, errMsg: Method parameter blogId: Failed to convert value of type java.lang.String to required type java.lang.Integer; For input string: \222222222222222222222222222222222222222\ } ]) def test_detail_login_fail(self, blog_id): token read_yml(data.yaml, user_token) header { User_token: token } r Request().get(urlself.url blogId str(blog_id[blogId]), headersheader) validate(instancer.json(), schemaself.Schema) assert r.json()[code] -1 assert r.json()[errMsg] blog_id[errMsg] def test_detail_login_fail_withoutBlogId(self): token read_yml(data.yaml, user_token) header { User_token: token } r Request().get(urlself.url, headersheader) validate(instancer.json(), schemaself.Schema) assert r.json()[code] -1 assert r.json()[errMsg] 参数校验失败测试在未登录状态下访问 状态码是否为401 正常登录下的输入正确测试用例数据之后按照接口测试用例中的数据输入异常登录的不同blogId数据测试是否符合预期要注意BlogId是数字不能将其正常和字符串拼接必须要将其强制转换成字符型在使用pytest.mark.parametrize装饰器参数化时可以内输入所需要的具体匹配数据来看看使用Request类请求所得的信息是否和期望数据一样这是单独分出来的一个异常测试用例在缺失关键参数字段的情况下正常访问接口所以要将中的url拆分成多个部分以方便按照设计的测试用例进行测试用户认证接口import pytest from jsonschema.validators import validate from utils.request_utils import host, Request from utils.yaml import read_yml class Test_getAuthorInfo: url host user/getAuthorInfo? Schema { type: object, properties: { code: { type: number }, errMsg: { type: [null, string] }, data: { type: [object, null], properties: { id: { type: number }, userName: { type: string }, githubUrl: { type: string } }, required: [ id, userName, githubUrl ] } }, required: [ code, errMsg, data ] } # 未登陆下访问 def test_get_authorInfo_no_login(self): url self.url blogId str(12222) r Request().get(urlurl) assert r.status_code 401 def test_get_authorInfo_login_success(self): BlogId read_yml(data.yaml, blogId) token read_yml(data.yaml, user_token) header { User_token: token } r Request().get(urlself.url blogId str(BlogId), headersheader) validate(instancer.json(), schemaself.Schema) assert r.json()[code] 200 assert r.json()[errMsg] is None pytest.mark.parametrize(blog_id, [ { blogId: 666666, errMsg: blogId 不合法 }, . . . . . ]) def test_get_authorInfo_login_fail(self, blog_id): token read_yml(data.yaml, user_token) header { User_token: token } r Request().get(urlself.url blogId str(blog_id[blogId]), headersheader) validate(instancer.json(), schemaself.Schema) assert r.json()[code] -1 assert r.json()[errMsg] blog_id[errMsg] def test_get_authorInfo_login_fail_withoutBlogId(self): token read_yml(data.yaml, user_token) header { User_token: token } r Request().get(urlself.url, headersheader) validate(instancer.json(), schemaself.Schema) assert r.json()[code] -1 assert r.json()[errMsg] 参数校验失败该接口和详情页接口的测试有很大一部分是一样的因为所需的参数都为blogId所以 只需按照postman中获取的json数据进行微调 Schema值也需适当调整。编辑页接口import pytest from jsonschema.validators import validate from utils.request_utils import host, Request from utils.yaml import read_yml class TestAdd: url host blog/add Schema { type: object, properties: { code: { type: number }, errMsg: { type: [null, string] }, data: { type: [boolean, null] } }, required: [ code, errMsg, data ] } # 未登录状态下访问 def test_add_no_login(self): r Request().post(urlself.url) assert r.status_code 401 # 登录状态下访问 pytest.mark.parametrize(add, [ { userId: 3, # 用户本人自己的Id title: 1111, content: 11111 }, . . . . . . ]) def test_add_login_success(self, add): token read_yml(data.yaml, user_token) header { User_token: token } json { userId: add[userId], title: add[title], content: add[content] } r Request().post(urlself.url, headersheader, jsonjson) validate(instancer.json(), schemaself.Schema) assert r.json()[code] 200 assert r.json()[errMsg] is None assert r.json()[data] is True # 异常用例 pytest.mark.parametrize(add_fail, [ { userId: 3, # 用户本人自己的Id title: , content: , errMsg: [博客正文不能为空, 标题不能为空] }, . . . . . . ]) def test_add_login_fail(self, add_fail): token read_yml(data.yaml, user_token) header { User_token: token } json { userId: add_fail[userId], title: add_fail[title], content: add_fail[content] } r Request().post(urlself.url, headersheader, jsonjson) validate(instancer.json(), schemaself.Schema) assert r.json()[errMsg] in add_fail[errMsg] assert r.json()[code] -1 assert r.json()[data] is None当postman出现“null或者ture这种Boolean类型的数据在判断时需要使用is来判断请求网页的json数据是否和对应的Schema数据匹配“null”对应Noneture对应“Ture”。用户信息接口import pytest from jsonschema.validators import validate from utils.request_utils import host, Request from utils.yaml import read_yml class Test_UserInfo: url host user/getUserInfo? Schema { type: object, properties: { code: { type: number }, errMsg: { type: [null, string] }, data: { type: [object, null], properties: { id: { type: number }, userName: { type: string }, githubUrl: { type: string } }, required: [ id, userName, githubUrl ] } }, required: [ code, errMsg, data ] } # 未登陆下访问 def test_get_authorInfo_no_login(self): url self.url userId str(12222) r Request().get(urlurl) assert r.status_code 401 def test_get_authorInfo_login_success(self): token read_yml(data.yaml, user_token) header { User_token: token } r Request().get(urlself.url userId str(3), headersheader) validate(instancer.json(), schemaself.Schema) assert r.json()[code] 200 assert r.json()[errMsg] is None pytest.mark.parametrize(user_id, [ { blogId: 666666, errMsg: Source must not be null }, . . . . . . ]) def test_get_authorInfo_login_fail(self, user_id): token read_yml(data.yaml, user_token) header { User_token: token } r Request().get(urlself.url userId str(user_id[blogId]), headersheader) validate(instancer.json(), schemaself.Schema) assert r.json()[code] -1 assert r.json()[errMsg] user_id[errMsg] def test_get_authorInfo_login_fail_withoutBlogId(self): token read_yml(data.yaml, user_token) header { User_token: token } r Request().get(urlself.url, headersheader) validate(instancer.json(), schemaself.Schema) assert r.json()[code] -1 assert r.json()[errMsg] 参数校验失败执行测试用例指定测试用例执行顺序如果用例的测试顺序不是我们想要的我们可以下载这个插件在测试类之前或者测试方法之前添加对应的代码即可按照指定顺序执行测试用例这里我们所必须要的是登陆凭证和所需的列表页中的blogId所以我们优先执行这两个接口测试用例。生成测试报告并分析结果我们需要下载Windows版Allure报告 下载地址https://github.com/allure-framework/allure2/releases/download/2.30.0/allure- 2.30.0.zip下载完成之后并解压之后需要将allure-2.30.0对应bin⽬录添加到系统环境变量中添加好之后重新打开pycharm我们需要在两个地方进行验证是否安装成功一个是cmd 一个是py控制台如果显示结果按照上面一样则说明下载成功接着输入上述指令 读取allure-results目录里的测试结果数据生成 HTML 报告输出到allure-reports目录。打开这个文件指定浏览器来查看本地报告测试执⾏时间从22:01:24持续到22:01:33总共耗时 8 秒 899 毫秒。测试时间与测试⽤例数量成正比⽤例数量越多测试时间越⻓。饼图显⽰了测试的通过率为 100%这意味着所有 55个测试⽤例都成功执⾏没有失败的测试⽤例。这里可以查看所有执行过的测试用例的详细信息 以及执行所需的时间。