目录
摘要
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 运行环境(JDK):allure 命令行工具的运行依赖,需配置 JAVA_HOME 环境变量
- allure 命令行工具:将测试结果数据转换为可视化的 HTML 报告,可通过 scoop install allure 安装,或手动下载后配置 Path 环境变量
三、搭建接口测试框架
在虚拟环境下编写代码 避免污染全局环境
1.测试架构
pytest.ini 的内容为addopts = -vs --alluredir allure-results
2.编写代码
(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(level=logging.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(filename=log_name, encoding="utf-8") info_handler = logging.FileHandler(filename=info_log_name, encoding="utf-8") err_handler = logging.FileHandler(filename=err_log_name, encoding="utf-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请求,url:"+url) self.log.info("接口信息:{}".format(kwargs)) r = requests.get(url=url, **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(url=url, **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, mode="a+", encoding="utf-8") as f: yaml.safe_dump(data, stream=f) # 在yaml文件中读取数据并返回需要的信息-token登录凭证 def read_yml(filename, key): with open(os.getcwd() + "/data/" + filename, mode="r", encoding="utf-8") as f: data = yaml.safe_load(stream=f) return data[key] # 清理yaml中的数据 def clear_yml(filename): with open(os.getcwd() + "/data/" + filename, mode="w", encoding="utf-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(url=self.url, json=json) validate(instance=r.json(), schema=self.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(url=self.url, json=json) validate(instance=r.json(), schema=self.schema) assert r.json()["code"] == -1 assert r.json()["data"] is None定义一个登录测试类------>未登录状态下访问测试用例,登录状态下访问的测试用例并分为正常测试用例和异常测试用例
使用pytest中内置的pytest.mark.parametrize装饰器对测试函数的参数进⾏参数化。以达到测试多组数据的目的
JSON Schema⼀个⽤来定义和校验JSON的web规范,简⽽⾔之,JSON Schema是⽤来校验json是否符合预期。
列表页接口:
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(url=self.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(url=self.url, headers=header) validate(instance=r.json(), schema=self.Schema) # 获取列表页的具体blogID data = { "blogId": r.json()["data"]["records"][0]["id"] } write_yml("data.yaml", data)详情页接口:
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(url=self.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(url=self.url + "blogId=" + str(BlogId), headers=header) validate(instance=r.json(), schema=self.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(url=self.url + "blogId=" + str(blog_id["blogId"]), headers=header) validate(instance=r.json(), schema=self.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(url=self.url, headers=header) validate(instance=r.json(), schema=self.Schema) assert r.json()["code"] == -1 assert r.json()["errMsg"] == "参数校验失败"用户认证接口:
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(url=url) 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(url=self.url + "blogId=" + str(BlogId), headers=header) validate(instance=r.json(), schema=self.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(url=self.url + "blogId=" + str(blog_id["blogId"]), headers=header) validate(instance=r.json(), schema=self.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(url=self.url, headers=header) validate(instance=r.json(), schema=self.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(url=self.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(url=self.url, headers=header, json=json) validate(instance=r.json(), schema=self.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(url=self.url, headers=header, json=json) validate(instance=r.json(), schema=self.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”对应None,ture对应“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(url=url) 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(url=self.url + "userId=" + str(3), headers=header) validate(instance=r.json(), schema=self.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(url=self.url + "userId=" + str(user_id["blogId"]), headers=header) validate(instance=r.json(), schema=self.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(url=self.url, headers=header) validate(instance=r.json(), schema=self.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
allure-results目录里的测试结果数据,生成 HTML 报告,输出到allure-reports目录。