基于 CrewAI、DeepSeek、在线 Embedding、Pydantic、SQLAlchemy、MySQL 与 OpenPyXL,实现 PRD 解析、测试点生成、测试用例生成、数据库入库和 Excel 导出。
文章目录
-
- [1. 项目目标](#1. 项目目标)
- [2. 核心技术与概念](#2. 核心技术与概念)
-
- [2.1 CrewAI](#2.1 CrewAI)
- [2.2 DeepSeek Chat](#2.2 DeepSeek Chat)
- [2.3 Embedding 与 RAG](#2.3 Embedding 与 RAG)
- [2.4 Pydantic](#2.4 Pydantic)
- [3. 项目目录](#3. 项目目录)
- [4. 安装与配置](#4. 安装与配置)
-
- [4.1 创建虚拟环境并安装依赖](#4.1 创建虚拟环境并安装依赖)
- [4.2 配置项](#4.2 配置项)
- [5. 第一步:定义结构化测试用例](#5. 第一步:定义结构化测试用例)
- [6. 第二步:使用 PDFSearchTool 解析 PRD](#6. 第二步:使用 PDFSearchTool 解析 PRD)
-
- [PRD 解析应该输出什么](#PRD 解析应该输出什么)
- [7. 第三步:创建三个 Agent](#7. 第三步:创建三个 Agent)
-
- [7.1 初始化 LLM](#7.1 初始化 LLM)
- [7.2 需求分析 Agent](#7.2 需求分析 Agent)
- [7.3 测试用例 Agent](#7.3 测试用例 Agent)
- [7.4 数据库 Agent](#7.4 数据库 Agent)
- [8. 第四步:创建 Task 并约束输出](#8. 第四步:创建 Task 并约束输出)
-
- [8.1 需求分析任务](#8.1 需求分析任务)
- [8.2 测试用例生成任务](#8.2 测试用例生成任务)
- [8.3 数据库任务](#8.3 数据库任务)
- [9. 第五步:用 SQLAlchemy 写入 MySQL](#9. 第五步:用 SQLAlchemy 写入 MySQL)
-
- [9.1 ORM 表模型](#9.1 ORM 表模型)
- [9.2 创建连接与会话](#9.2 创建连接与会话)
- [9.3 统一处理 Pydantic 对象与字典](#9.3 统一处理 Pydantic 对象与字典)
- [9.4 封装为 CrewAI Tool](#9.4 封装为 CrewAI Tool)
- 重复执行问题
- [10. 第六步:导出 Excel](#10. 第六步:导出 Excel)
- [11. 第七步:编排并执行完整流程](#11. 第七步:编排并执行完整流程)
- [12. 推荐的完整入口结构](#12. 推荐的完整入口结构)
- [13. 如何运行](#13. 如何运行)
-
- [13.1 完整源码](#13.1 完整源码)
- [13.2 准备 MySQL](#13.2 准备 MySQL)
- [13.3 从项目根目录启动](#13.3 从项目根目录启动)
- [13.4 验证结果](#13.4 验证结果)
- [14. 常见报错与解决方法](#14. 常见报错与解决方法)
-
- [14.1 `No module named 'pymysql'`](#14.1
No module named 'pymysql') - [14.2 `OPENAI_API_KEY environment variable is not set`](#14.2
OPENAI_API_KEY environment variable is not set) - [14.3 `Model does not exist`](#14.3
Model does not exist) - [14.4 Agent 的 `llm` 校验失败](#14.4 Agent 的
llm校验失败) - [14.5 `'dict' object has no attribute 'model_dump'`](#14.5
'dict' object has no attribute 'model_dump') - [14.6 `'list' object has no attribute 'test_case_list'`](#14.6
'list' object has no attribute 'test_case_list') - [14.7 程序显示完成,但数据库没有数据](#14.7 程序显示完成,但数据库没有数据)
- [14.8 PDF 找不到](#14.8 PDF 找不到)
- [14.9 Excel 无法保存](#14.9 Excel 无法保存)
- [14.1 `No module named 'pymysql'`](#14.1
- [15. 从 PDF 扩展到 DOC 和数据库需求](#15. 从 PDF 扩展到 DOC 和数据库需求)
- [16. 测试用例设计方法如何写入 Prompt](#16. 测试用例设计方法如何写入 Prompt)
- [17. 当前方案的改进方向](#17. 当前方案的改进方向)
-
- [17.1 把"生成"和"落库"适度解耦](#17.1 把“生成”和“落库”适度解耦)
- [17.2 增加业务校验](#17.2 增加业务校验)
- [17.3 可观测性](#17.3 可观测性)
- [17.4 稳定性与维护](#17.4 稳定性与维护)
- [18. 最终知识链](#18. 最终知识链)
1. 项目目标
本项目希望把测试设计中的重复工作串成一条自动化流水线:
- 获取需求文档,例如 PDF、Word 或数据库中的需求数据。
- 由需求分析智能体读取并拆解需求,形成可测试的需求点。
- 由测试用例智能体运用等价类、边界值、场景法等方法生成用例。
- 使用 Pydantic 约束大模型输出,得到稳定的结构化数据。
- 由数据库工具把测试用例批量写入 MySQL。
- 同时把结构化测试用例导出为 Excel,方便人工评审和流转。
完整链路如下:
#mermaid-svg-HhDifaNpO4lZ2bE1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HhDifaNpO4lZ2bE1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HhDifaNpO4lZ2bE1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HhDifaNpO4lZ2bE1 .error-icon{fill:#552222;}#mermaid-svg-HhDifaNpO4lZ2bE1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HhDifaNpO4lZ2bE1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HhDifaNpO4lZ2bE1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HhDifaNpO4lZ2bE1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HhDifaNpO4lZ2bE1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HhDifaNpO4lZ2bE1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HhDifaNpO4lZ2bE1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HhDifaNpO4lZ2bE1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HhDifaNpO4lZ2bE1 .marker.cross{stroke:#333333;}#mermaid-svg-HhDifaNpO4lZ2bE1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HhDifaNpO4lZ2bE1 p{margin:0;}#mermaid-svg-HhDifaNpO4lZ2bE1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HhDifaNpO4lZ2bE1 .cluster-label text{fill:#333;}#mermaid-svg-HhDifaNpO4lZ2bE1 .cluster-label span{color:#333;}#mermaid-svg-HhDifaNpO4lZ2bE1 .cluster-label span p{background-color:transparent;}#mermaid-svg-HhDifaNpO4lZ2bE1 .label text,#mermaid-svg-HhDifaNpO4lZ2bE1 span{fill:#333;color:#333;}#mermaid-svg-HhDifaNpO4lZ2bE1 .node rect,#mermaid-svg-HhDifaNpO4lZ2bE1 .node circle,#mermaid-svg-HhDifaNpO4lZ2bE1 .node ellipse,#mermaid-svg-HhDifaNpO4lZ2bE1 .node polygon,#mermaid-svg-HhDifaNpO4lZ2bE1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HhDifaNpO4lZ2bE1 .rough-node .label text,#mermaid-svg-HhDifaNpO4lZ2bE1 .node .label text,#mermaid-svg-HhDifaNpO4lZ2bE1 .image-shape .label,#mermaid-svg-HhDifaNpO4lZ2bE1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-HhDifaNpO4lZ2bE1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HhDifaNpO4lZ2bE1 .rough-node .label,#mermaid-svg-HhDifaNpO4lZ2bE1 .node .label,#mermaid-svg-HhDifaNpO4lZ2bE1 .image-shape .label,#mermaid-svg-HhDifaNpO4lZ2bE1 .icon-shape .label{text-align:center;}#mermaid-svg-HhDifaNpO4lZ2bE1 .node.clickable{cursor:pointer;}#mermaid-svg-HhDifaNpO4lZ2bE1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HhDifaNpO4lZ2bE1 .arrowheadPath{fill:#333333;}#mermaid-svg-HhDifaNpO4lZ2bE1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HhDifaNpO4lZ2bE1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HhDifaNpO4lZ2bE1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HhDifaNpO4lZ2bE1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HhDifaNpO4lZ2bE1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HhDifaNpO4lZ2bE1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HhDifaNpO4lZ2bE1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HhDifaNpO4lZ2bE1 .cluster text{fill:#333;}#mermaid-svg-HhDifaNpO4lZ2bE1 .cluster span{color:#333;}#mermaid-svg-HhDifaNpO4lZ2bE1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HhDifaNpO4lZ2bE1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HhDifaNpO4lZ2bE1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-HhDifaNpO4lZ2bE1 .icon-shape,#mermaid-svg-HhDifaNpO4lZ2bE1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HhDifaNpO4lZ2bE1 .icon-shape p,#mermaid-svg-HhDifaNpO4lZ2bE1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HhDifaNpO4lZ2bE1 .icon-shape .label rect,#mermaid-svg-HhDifaNpO4lZ2bE1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HhDifaNpO4lZ2bE1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HhDifaNpO4lZ2bE1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HhDifaNpO4lZ2bE1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 需求来源
DOC / PDF / 数据库
需求检索工具
RAG
需求分析 Agent
PRD 解析与测试点生成
测试用例 Agent
用例设计与格式化
Pydantic
结构校验
数据库 Agent + Tool
MySQL
用例管理库
OpenPyXL
Excel 测试用例
示例需求文档是"管理员登录接口文档",核心内容包括:POST /admin/employee/login、用户名和密码校验、账号禁用、成功/失败响应以及 JWT 有效期等。这类接口文档非常适合用于演示边界值、等价类和业务场景测试设计。
通俗点来说,我们要实现的流程:
获取需求文档(doc、pdf、数据库数据) --> 智能体(需求分解) --> 智能体(编写用例、格式化) --> 智能体(插入到数据库表--用例管理数据库)
2. 核心技术与概念
2.1 CrewAI
CrewAI 用于定义多个具有不同职责的智能体,并把任务按顺序编排起来。
| 概念 | 可以理解为 | 本项目中的用途 |
|---|---|---|
| Agent | 承担某种职责的人 | 需求分析工程师、测试用例工程师、数据库工程师 |
| Task | 分配给人的具体工作 | 解析 PRD、编写用例、执行入库 |
| Tool | 人可以调用的外部能力 | PDF 检索、MySQL 数据写入 |
| Crew | 项目团队及工作流 | 按任务顺序执行三个 Agent |
| LLM | Agent 的推理与生成能力 | DeepSeek Chat 或服务商提供的兼容模型 |
Agent 不等于一段固定函数。它会结合角色、目标、背景、当前任务和工具,自主决定如何完成任务。因此,涉及数据库写入等确定性操作时,必须让 Tool 负责真正的数据操作,不能只相信 Agent 的自然语言回复。
2.2 DeepSeek Chat
本项目通过 OpenAI 兼容协议连接模型服务,目标大语言模型为 deepseek-chat。不同服务商的模型 ID 可能不同,应填写服务商文档中真实存在的 ID,例如服务商可能要求 deepseek/deepseek-chat 或其他完整名称。
大语言模型负责:
- 理解接口或 PRD 的业务语义;
- 把自然语言需求拆成测试点;
- 结合测试设计方法生成用例;
- 按 Pydantic 模型返回 JSON。
2.3 Embedding 与 RAG
PDFSearchTool 不是把整份 PDF 一次性放进提示词,而是使用 RAG:
- 读取 PDF 并切分文本块;
- 调用在线 Embedding 模型,把文本转换成向量;
- 将文本及向量保存到向量数据库;
- Agent 提问时进行语义检索;
- 把最相关的片段交给大语言模型分析。
Embedding 模型与聊天模型用途不同。聊天模型生成文字,Embedding 模型生成向量,不能把 deepseek-chat 当作 Embedding 模型使用。在线 Embedding 模型必须是服务商实际支持的向量模型。
2.4 Pydantic
大模型自由输出时,字段名称、数据类型和层级可能不稳定。Pydantic 相当于数据合同:只有符合合同的数据才能进入数据库和 Excel。
python
from typing import List
from pydantic import BaseModel, Field
class TestCaseInfo(BaseModel):
"""一条测试用例。"""
case_id: str = Field(..., description="测试用例 ID,例如 TC_LOGIN_001")
module: str = Field(..., description="所属模块,例如用户登录模块")
title: str = Field(..., description="测试标题")
priority: str = Field(..., description="优先级:P0、P1、P2、P3")
precondition: str = Field(..., description="前置条件")
steps: List[str] = Field(..., description="测试步骤列表")
expected_result: str = Field(..., description="预期结果")
actual_result: str = Field(..., description="实际结果,未执行时可填未执行")
class TestCaseInfoList(BaseModel):
"""测试用例集合。"""
test_case_list: List[TestCaseInfo] = Field(
...,
description="测试用例列表",
)
TestCaseInfoList 这一层包装很重要。它给整个输出提供固定根节点 test_case_list,便于 CrewAI、数据库工具和 Excel 模块统一取值。
3. 项目目录
text
fastApiProject1/
├─ .env # 本机配置,不提交、不打印
├─ AIAgent/
│ ├─ __init__.py
│ ├─ ai_agents.py # Agent、Task、Crew 编排及程序入口
│ ├─ models.py # Pydantic 输出模型
│ ├─ db.py # SQLAlchemy ORM 与 CrewAI 数据库工具
│ ├─ excel_export.py # Excel 导出
│ ├─ api_doc.pdf # 示例需求文档
│ └─ exports/ # 自动生成的 Excel 文件
└─ venv/

建议统一使用包导入,并从项目根目录以模块方式启动。这样不会因为当前工作目录不同而出现 ModuleNotFoundError。
python
# 推荐在 AIAgent 内统一使用
from AIAgent.models import TestCaseInfo, TestCaseInfoList
from AIAgent.db import DBTool
from AIAgent.excel_export import export_test_cases
4. 安装与配置
4.1 创建虚拟环境并安装依赖
在项目根目录执行:
powershell
python -m venv venv
.\venv\Scripts\python.exe -m pip install crewai crewai-tools pydantic sqlalchemy pymysql python-dotenv openpyxl
要点:安装依赖和运行程序必须使用同一个 Python。若用系统 Python 安装、却用 venv 运行,程序仍然找不到包。
4.2 配置项
不要在代码中最好不要直接填写真实密钥,我们可以用env文件进行管理:
env文件内容:
dotenv
# 聊天模型
SILICONFLOW_API_KEY=your_llm_api_key
OPENAI_API_BASE=https://your-provider.example/v1
LLM_MODEL=deepseek-chat
# 在线向量模型
EMBEDDING_API_KEY=your_embedding_api_key
EMBEDDING_BASE_URL=https://your-provider.example/v1
EMBEDDING_MODEL=your_supported_embedding_model
# MySQL
MYSQL_USER=root
MYSQL_PASSWORD=your_mysql_password
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_DATABASE=test_platform

这里我用的是硅基流动,大家可以按照自己的需求进行修改。
安全要求:
.env加入.gitignore;- 不打印 API Key 和数据库密码;
- 线上环境优先使用密钥管理服务;
- 排查配置时只检查变量是否存在,不输出变量值。
5. 第一步:定义结构化测试用例
文件:AIAgent/models.py
Pydantic 在本项目中承担三项职责:
- 给大模型明确输出字段;
- 在程序运行时验证字段和类型;
- 为数据库与 Excel 提供统一的数据对象。
例如大模型生成以下 JSON 后,可直接验证:
python
payload = {
"test_case_list": [
{
"case_id": "TC_LOGIN_001",
"module": "管理员登录",
"title": "正确用户名和密码登录",
"priority": "P0",
"precondition": "管理员账号存在且未被禁用",
"steps": [
"发送 POST /admin/employee/login",
"username 填写 admin",
"password 填写正确密码",
"提交请求",
],
"expected_result": "code 为 1,返回管理员信息和 JWT token",
"actual_result": "未执行",
}
]
}
test_cases = TestCaseInfoList.model_validate(payload)
print(test_cases.test_case_list[0].case_id)
如果字段缺失、steps 不是列表或数据层级错误,Pydantic 会立即抛出校验异常,避免脏数据继续向下传递。
6. 第二步:使用 PDFSearchTool 解析 PRD
python
from pathlib import Path
from crewai_tools import PDFSearchTool
pdf_path = Path(__file__).with_name("api_doc.pdf")
tool_pdf = PDFSearchTool(
pdf=str(pdf_path),
config={
"embedding_model": {
"provider": "openai",
"config": {
"api_key": EMBEDDING_API_KEY,
"model_name": EMBEDDING_MODEL,
"api_base": EMBEDDING_BASE_URL,
},
},
},
)
这里使用 Path(__file__).with_name("api_doc.pdf"),PDF 路径始终相对于代码文件定位。即使从 PyCharm、命令行或不同目录启动,也不会误找当前工作目录下的文件。
config 中必须使用当前 CrewAI Tools 版本能够识别的键。若配置没有生效,工具可能退回默认 OpenAI Embedding,并继续寻找 OPENAI_API_KEY。
PRD 解析应该输出什么
仅输出"需求摘要"还不够。一个适合生成用例的需求分析结果至少应包含:
- 功能目标和参与角色;
- 输入字段、类型、必填性和边界;
- 正常业务流程;
- 异常分支和错误提示;
- 状态约束;
- 权限与安全要求;
- 接口路径、方法、请求和响应结构;
- 文档中的歧义、缺失或待确认项;
- 可以采用的测试设计方法。
对于管理员登录示例,可提炼出如下测试点:
| 测试维度 | 测试点 |
|---|---|
| 正常流程 | 正确用户名、正确密码、账号启用时登录成功 |
| 用户名 | 为空、1 位、20 位、21 位、特殊字符、大小写、用户不存在 |
| 密码 | 为空、5 位、6 位、20 位、21 位、错误密码、特殊字符 |
| 账号状态 | 已启用、已禁用 |
| 响应 | code、data、msg 类型和业务含义正确 |
| Token | 登录成功返回 JWT、有效期约 2 小时、过期或伪造 Token 被拒绝 |
| 安全 | HTTPS、日志不记录明文密码和完整 Token |
7. 第三步:创建三个 Agent
7.1 初始化 LLM
CrewAI 的 Agent.llm 应传入 CrewAI 自己的 LLM 对象或其支持的模型字符串。当前实现采用 LLM:
python
from crewai import LLM
llm = LLM(
model=MODEL,
api_key=LLM_API_KEY,
base_url=BASE_URL,
custom_openai=True,
temperature=0.7,
)
不建议直接把 LangChain 的 ChatOpenAI 实例传给新版 CrewAI Agent,因为不同版本的类型系统可能不兼容。
7.2 需求分析 Agent
python
requirements_analysis_agent = Agent(
role="软件测试需求分析工程师",
goal="读取 PDF,提取业务规则、输入约束、异常分支和测试点",
backstory="你擅长把产品需求和接口文档转换成可测试的需求列表。",
tools=[tool_pdf],
verbose=True,
allow_delegation=False,
llm=llm,
)
7.3 测试用例 Agent
python
testcase_writer_agent = Agent(
role="软件测试用例编写工程师",
goal="根据需求列表编写完整、可执行、结构化的测试用例",
backstory="你熟悉等价类、边界值、判定表、场景法和错误推测法。",
verbose=True,
allow_delegation=False,
llm=llm,
)
7.4 数据库 Agent
python
db_agent = Agent(
role="数据库操作工程师",
goal="调用数据库工具,将全部测试用例准确写入 MySQL",
backstory="你只通过已提供的工具执行数据写入,并返回真实执行结果。",
tools=[tool_db],
verbose=True,
allow_delegation=False,
llm=llm,
)
数据库 Agent 的文本回复不代表数据库真的发生了变化。只有工具成功完成事务,并返回插入数量和 ID,才算入库成功。
8. 第四步:创建 Task 并约束输出
8.1 需求分析任务
python
task_requirements = Task(
description="""
检索需求文档并分解需求。输出:功能目标、接口信息、输入约束、
正常流程、异常流程、状态规则、安全要求、测试点和待确认项。
""",
expected_output="分组明确、可以直接用于测试设计的中文需求与测试点列表",
agent=requirements_analysis_agent,
)
8.2 测试用例生成任务
python
task_testcase = Task(
description="""
根据需求分析结果生成中文测试用例。覆盖正常、异常、边界、状态、
接口响应和安全场景。case_id 必须唯一,steps 必须是字符串列表,
actual_result 在尚未执行时统一填写"未执行"。
""",
expected_output="符合 TestCaseInfoList 模型的完整测试用例列表",
output_json=TestCaseInfoList,
context=[task_requirements],
agent=testcase_writer_agent,
)
output_json=TestCaseInfoList 会要求 CrewAI 将最终输出解析为指定结构。context=[task_requirements] 明确说明本任务依赖需求分析任务的结果,比仅依赖任务排列更清晰。
8.3 数据库任务
python
task_db = Task(
description="""
从测试用例生成任务的结果中读取 test_case_list,调用 db_tool,
将列表中的每一条测试用例写入数据库。不得只回复成功,必须调用工具。
""",
expected_output="数据库工具返回的实际插入数量和测试用例 ID",
context=[task_testcase],
agent=db_agent,
)
不同 CrewAI 版本对 Task 参数的支持可能变化。显式使用 context 传递上游输出更容易理解;最终仍应以当前安装版本的官方接口和运行结果为准。
9. 第五步:用 SQLAlchemy 写入 MySQL
9.1 ORM 表模型
python
from sqlalchemy import JSON, String, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class TestCaseRecord(Base):
__tablename__ = "test_case_info"
case_id: Mapped[str] = mapped_column(String(50), primary_key=True)
module: Mapped[str] = mapped_column(String(100), nullable=False)
title: Mapped[str] = mapped_column(String(255), nullable=False)
priority: Mapped[str] = mapped_column(String(10), nullable=False)
precondition: Mapped[str] = mapped_column(Text, nullable=False)
steps: Mapped[list[str]] = mapped_column(JSON, nullable=False)
expected_result: Mapped[str] = mapped_column(Text, nullable=False)
actual_result: Mapped[str] = mapped_column(Text, nullable=False)
steps 是多步操作,使用 MySQL JSON 类型比把 Python 列表强行转成普通字符串更清晰。读取后也更容易还原成列表。
9.2 创建连接与会话
python
import os
from sqlalchemy import create_engine
from sqlalchemy.engine import URL
from sqlalchemy.orm import sessionmaker
DATABASE_URL = URL.create(
drivername="mysql+pymysql",
username=os.getenv("MYSQL_USER", "root"),
password=os.getenv("MYSQL_PASSWORD"),
host=os.getenv("MYSQL_HOST", "127.0.0.1"),
port=int(os.getenv("MYSQL_PORT", "3306")),
database=os.getenv("MYSQL_DATABASE", "test_platform"),
query={"charset": "utf8mb4"},
)
engine = create_engine(DATABASE_URL, pool_pre_ping=True)
SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)
生产项目不建议在代码中保留真实默认密码。数据库名需要提前创建,Base.metadata.create_all() 只创建表,不创建 MySQL 数据库本身。
9.3 统一处理 Pydantic 对象与字典
CrewAI 在调用工具时,Pydantic 对象往往会经过 JSON 序列化,工具最终收到的是 dict。因此不能假定每一项都有 .model_dump()。
python
from typing import Any
def normalize_test_cases(
test_cases: list[TestCaseInfo | dict[str, Any]],
) -> list[TestCaseInfo]:
return [
item
if isinstance(item, TestCaseInfo)
else TestCaseInfo.model_validate(item)
for item in test_cases
]
这一步正是解决下面错误的关键:
text
'dict' object has no attribute 'model_dump'
统一转换后,再批量入库:
python
def insert_test_cases(
test_cases: list[TestCaseInfo | dict[str, Any]],
) -> list[str]:
validated = normalize_test_cases(test_cases)
records = [
TestCaseRecord(**test_case.model_dump())
for test_case in validated
]
if not records:
return []
with SessionLocal.begin() as session:
session.add_all(records)
return [record.case_id for record in records]
SessionLocal.begin() 会在代码块成功结束时提交事务,发生异常时自动回滚。因此,不会出现忘记调用 commit() 导致界面中看不到数据的问题。
9.4 封装为 CrewAI Tool
python
from typing import Type
from crewai.tools import BaseTool
from pydantic import BaseModel
class DBToolInput(BaseModel):
testcaselist: list[TestCaseInfo]
class DBTool(BaseTool):
name: str = "db_tool"
description: str = "将测试用例列表中的每条数据插入 MySQL"
args_schema: Type[BaseModel] = DBToolInput
def _run(
self,
testcaselist: list[TestCaseInfo | dict],
) -> str:
Base.metadata.create_all(bind=engine)
inserted_ids = insert_test_cases(testcaselist)
return (
f"成功插入 {len(inserted_ids)} 条测试用例,"
f"测试用例 ID:{inserted_ids}"
)
工具返回真实插入数量与 ID,便于判断入库是否真正成功。
重复执行问题
case_id 是主键。同一批用例重复运行时,可能出现主键冲突。这不是数据库失效,而是数据已存在。可根据业务选择:
- 每次生成全新的唯一
case_id; - 插入前判断是否存在;
- 使用 MySQL Upsert 更新已有用例;
- 在测试环境明确清理旧数据后再运行。
不要为了省事直接吞掉异常,否则 Agent 可能声称"完成",但数据库实际已经回滚。
10. 第六步:导出 Excel
Excel 导出不需要再交给大模型。测试用例已经是结构化数据,直接用确定性代码生成更可靠。
python
from datetime import datetime
from pathlib import Path
from typing import Any, Iterable
from openpyxl import Workbook
HEADERS = [
"测试用例ID",
"所属模块",
"测试标题",
"优先级",
"前置条件",
"测试步骤",
"预期结果",
"实际结果",
]
def export_test_cases(
test_cases: Iterable[TestCaseInfo | dict[str, Any]],
output_path: str | Path | None = None,
) -> str:
validated = [
item
if isinstance(item, TestCaseInfo)
else TestCaseInfo.model_validate(item)
for item in test_cases
]
if not validated:
raise ValueError("测试用例列表为空,无法导出 Excel")
if output_path is None:
output_dir = Path(__file__).resolve().parent / "exports"
output_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
destination = output_dir / f"测试用例_{timestamp}.xlsx"
else:
destination = Path(output_path).expanduser().resolve()
destination.parent.mkdir(parents=True, exist_ok=True)
workbook = Workbook()
worksheet = workbook.active
worksheet.title = "测试用例"
worksheet.freeze_panes = "A2"
worksheet.append(HEADERS)
for test_case in validated:
steps = "\n".join(
f"{index}. {step}"
for index, step in enumerate(test_case.steps, start=1)
)
worksheet.append([
test_case.case_id,
test_case.module,
test_case.title,
test_case.priority,
test_case.precondition,
steps,
test_case.expected_result,
test_case.actual_result,
])
workbook.save(destination)
return str(destination)
当前项目的完整版本还包含自动换行、列宽、表头样式、筛选以及 Excel 公式注入防护。安全处理尤其重要:若单元格内容以 = + - @ 开头,应当作为普通文本写入,避免被 Excel 当成公式执行。
11. 第七步:编排并执行完整流程
python
from crewai import Crew, Process
crew = Crew(
agents=[
requirements_analysis_agent,
testcase_writer_agent,
db_agent,
],
tasks=[
task_requirements,
task_testcase,
task_db,
],
process=Process.sequential,
verbose=True,
)
result = crew.kickoff()
print(result)
顺序模式的执行过程是:
text
task_requirements
↓ 需求与测试点
task_testcase
↓ TestCaseInfoList
task_db
↓ 调用 db_tool 写入 MySQL
数据库任务结束后,再从测试用例任务取得结构化结果并导出 Excel:
python
if task_testcase.output is None:
raise RuntimeError("测试用例任务没有输出")
if task_testcase.output.pydantic is not None:
test_case_data = TestCaseInfoList.model_validate(
task_testcase.output.pydantic.model_dump()
)
elif task_testcase.output.json_dict is not None:
test_case_data = TestCaseInfoList.model_validate(
task_testcase.output.json_dict
)
else:
raise RuntimeError("测试用例任务没有生成结构化数据")
excel_path = export_test_cases(test_case_data.test_case_list)
print(f"测试用例已导出到:{excel_path}")
这里直接读取 task_testcase.output,而不是从整条 Crew 的最终自然语言回复中重新解析 JSON。这样可以避免数据库 Agent 的文本把测试用例结果覆盖掉。
12. 推荐的完整入口结构
初始化 PDFSearchTool 时会执行 PDF 解析和向量化,属于有成本且可能失败的外部操作。建议把启动逻辑放进函数,并增加配置检查:
python
import os
from pathlib import Path
from crewai import Agent, Crew, LLM, Process, Task
from crewai_tools import PDFSearchTool
from dotenv import find_dotenv, load_dotenv
from AIAgent.db import DBTool
from AIAgent.excel_export import export_test_cases
from AIAgent.models import TestCaseInfoList
def require_env(name: str) -> str:
value = os.getenv(name)
if not value:
raise RuntimeError(f"缺少配置项:{name}")
return value
def main() -> None:
load_dotenv(find_dotenv())
llm = LLM(
model=require_env("LLM_MODEL"),
api_key=require_env("SILICONFLOW_API_KEY"),
base_url=require_env("OPENAI_API_BASE"),
custom_openai=True,
temperature=0.7,
)
pdf_tool = PDFSearchTool(
pdf=str(Path(__file__).with_name("api_doc.pdf")),
config={
"embedding_model": {
"provider": "openai",
"config": {
"api_key": require_env("EMBEDDING_API_KEY"),
"model_name": require_env("EMBEDDING_MODEL"),
"api_base": require_env("EMBEDDING_BASE_URL"),
},
},
},
)
db_tool = DBTool()
# 在此创建三个 Agent 和三个 Task,代码与前文一致。
crew = Crew(
agents=[requirements_analysis_agent, testcase_writer_agent, db_agent],
tasks=[task_requirements, task_testcase, task_db],
process=Process.sequential,
verbose=True,
)
crew.kickoff()
output = task_testcase.output
if output is None:
raise RuntimeError("测试用例任务没有输出")
raw_data = (
output.pydantic.model_dump()
if output.pydantic is not None
else output.json_dict
)
if raw_data is None:
raise RuntimeError("测试用例任务没有结构化输出")
data = TestCaseInfoList.model_validate(raw_data)
path = export_test_cases(data.test_case_list)
print(f"Excel 文件:{path}")
if __name__ == "__main__":
main()
require_env() 只报告哪个配置项缺失,不输出密钥值,既方便排查也不会泄露敏感信息。
13. 如何运行
13.1 完整源码
ai_agents.py文件:
python
import os
from pathlib import Path
from crewai import Agent, Task, Crew, LLM
from dotenv import load_dotenv, find_dotenv
from crewai_tools import PDFSearchTool
from AIAgent.db import DBTool
from AIAgent.excel_export import export_test_cases
from models import TestCaseInfoList
# 加载 .env 文件
load_dotenv(find_dotenv())
# 硅基流动 API Key
LLM_API_KEY = os.getenv("SILICONFLOW_API_KEY")
# 硅基流动 OpenAI 兼容接口
BASE_URL = os.getenv("OPENAI_API_BASE")
# 模型
MODEL = os.getenv("LLM_MODEL")
# 嵌入式模型
EMBEDDING_BASE_URL = os.getenv("EMBEDDING_BASE_URL")
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL")
EMBEDDING_API_KEY = os.getenv("EMBEDDING_API_KEY")
# 初始化大语言模型
llm = LLM(
model=MODEL, # 关键: 必须包含服务商 deepseek,然后再写模型名 deepseek-v4-flash 或 deepseek-v4-pro
api_key=LLM_API_KEY, # 设置 API key
base_url=BASE_URL,
custom_openai=True,
temperature=0.7,
)
# 该工具完成文档的嵌入和数据的检索
pdf_path = Path(__file__).with_name("api_doc.pdf")
tool_pdf = PDFSearchTool(
pdf=str(pdf_path),
config={
"embedding_model": {
"provider": "openai",
"config": {
"api_key": EMBEDDING_API_KEY,
"model_name": EMBEDDING_MODEL,
"api_base": EMBEDDING_BASE_URL,
},
},
},
)
# 创建数据库操作工具
tool_db = DBTool()
# -------------------------------- 创建智能体 --------------------------------
# 智能体(需求获取然后分解)
# 创建一个需求文档读取的智能体,该智能体通过调用外部工具完成文档内容的获取
requirements_analysis_agent = Agent(
role = "软件测试需求分析工程师",
goal = "读取PDF文件,分析其中的测试需求",
backstory = "你是一位根据用户提供的PDF文件获取测试需求的工程师,请让你的回答尽可能详细",
tools = [tool_pdf], # 工具列表,可以添加多个工具
verbose =True,
allow_delegation=False, # 如果这个angent处理不了这个任务,不要交给下一个agent去做
llm = llm,
)
# 智能体编写测试用例
testcase_writer_agent = Agent(
role = "软件测试用例编写工程师",
goal = "根据需求列表,编写测试用例",
backstory = "你是一位专业的根据需求文档编写测试用例的工程师,请让你的回答尽可能详细",
allow_delegation=False, # 如果这个angent处理不了这个任务,不要交给下一个agent去做
verbose =True,
llm = llm,
)
# 创建连接数据库,插入数据库信息的智能体
db_agent = Agent(
role = "数据库操作工程师",
goal = "将测试用例信息插入数据库",
backstory = "你是一位专业的数据库操作工程师,不要回复,直接将数据插入数据库即可",
tools = [tool_db],
verbose =True,
allow_delegation=False, # 如果这个angent处理不了这个任务,不要交给下一个agent去做
llm = llm,
)
# -------------------------------- 创建任务 --------------------------------
# 创建需求分析任务,传入提示词
task_requirements = Task(
description = "分解出详细的需求条目",
expected_output = "详细的需求列表",
agent = requirements_analysis_agent # 指定智能体
)
# 创建编写测试用例的任务
task_testcase = Task(
description = """根据需求列表,编写测试用例,尽可能覆盖到多种用例设计方法,输出语言为中文。每条测试用例的属性信息如下:
case_id: str = Field(...,description="测试编号,也就是测试用例ID,例如 TC_LOGIN_001")
module: str = Field(...,description="所属模块,例如 用户登录模块")
title: str = Field(...,description="测试标题,例如 正确用户名和密码登录")
priority: str = Field(...,description="测试优先级,例如 P0、P1、P2、P3")
precondition: str = Field(...,description="执行测试用例之前需要满足的前提条件")
steps: List[str] = Field(...,description="测试步骤,可以包含多个操作步骤")
expected_result: str = Field(...,description="执行测试步骤后应该得到的预期结果")
actual_result: str = Field(..., description="执行测试步骤后实际得到的预期结果")""",
expected_output = "测试用例列表",
output_json = TestCaseInfoList,
agent = testcase_writer_agent # 指定智能体
)
# 创建数据库操作任务
task_db = Task(
description = "将测试用例数据插入到数据库",
expected_output = "测试用例信息已插入数据库",
input_json = TestCaseInfoList,
agent = db_agent # 指定智能体
)
# 编排,把智能体和任务融合在一起
crew = Crew(
agents = [requirements_analysis_agent,testcase_writer_agent,db_agent],
tasks = [task_requirements, task_testcase, task_db],
verbose = True
)
result = crew.kickoff()
print(result)
# 使用测试用例编写任务的结构化结果导出 Excel,避免依赖智能体文本回复。
if task_testcase.output is None:
raise RuntimeError("测试用例任务没有输出,无法导出 Excel")
if task_testcase.output.pydantic is not None:
test_case_data = TestCaseInfoList.model_validate(
task_testcase.output.pydantic.model_dump()
)
elif task_testcase.output.json_dict is not None:
test_case_data = TestCaseInfoList.model_validate(
task_testcase.output.json_dict
)
else:
raise RuntimeError("测试用例任务没有生成结构化数据,无法导出 Excel")
excel_path = export_test_cases(test_case_data.test_case_list)
print(f"测试用例已导出到:{excel_path}")
db.py文件:
python
"""测试用例 MySQL 数据库操作模块。"""
import os
from typing import Any, List, Type
from crewai.tools import BaseTool
from pydantic import BaseModel
from sqlalchemy import JSON, String, Text, create_engine
from sqlalchemy.engine import URL
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker
from models import TestCaseInfo
class Base(DeclarativeBase):
"""SQLAlchemy ORM 基类。"""
class TestCaseRecord(Base):
"""测试用例数据库表。"""
__tablename__ = "test_case_info"
case_id: Mapped[str] = mapped_column(
String(50),
primary_key=True,
comment="测试用例ID",
)
module: Mapped[str] = mapped_column(
String(100),
nullable=False,
comment="所属模块",
)
title: Mapped[str] = mapped_column(
String(255),
nullable=False,
comment="测试标题",
)
priority: Mapped[str] = mapped_column(
String(10),
nullable=False,
comment="测试优先级",
)
precondition: Mapped[str] = mapped_column(
Text,
nullable=False,
comment="前置条件",
)
steps: Mapped[list[str]] = mapped_column(
JSON,
nullable=False,
comment="测试步骤",
)
expected_result: Mapped[str] = mapped_column(
Text,
nullable=False,
comment="预期结果",
)
actual_result: Mapped[str] = mapped_column(
Text,
nullable=False,
comment="实际结果",
)
DATABASE_URL = URL.create(
drivername="mysql+pymysql",
username=os.getenv("MYSQL_USER", "root"),
password=os.getenv("MYSQL_PASSWORD", "123456"),
host=os.getenv("MYSQL_HOST", "127.0.0.1"),
port=int(os.getenv("MYSQL_PORT", "3306")),
database=os.getenv("MYSQL_DATABASE", "test_platform"),
query={"charset": "utf8mb4"},
)
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True,
)
SessionLocal = sessionmaker(
bind=engine,
expire_on_commit=False,
)
def create_tables() -> None:
"""创建数据库表;已经存在的表不会重复创建。"""
Base.metadata.create_all(bind=engine)
def normalize_test_cases(test_cases: List[TestCaseInfo | dict[str, Any]]) -> list[TestCaseInfo]:
"""将 CrewAI 传入的字典列表统一转换为 TestCaseInfo 列表。"""
return [
test_case
if isinstance(test_case, TestCaseInfo)
else TestCaseInfo.model_validate(test_case)
for test_case in test_cases
]
def insert_test_cases(
test_cases: List[TestCaseInfo | dict[str, Any]],
) -> list[str]:
"""将测试用例列表中的全部数据插入数据库。"""
validated_test_cases = normalize_test_cases(test_cases)
records = [
TestCaseRecord(**test_case.model_dump())
for test_case in validated_test_cases
]
if not records:
return []
with SessionLocal.begin() as session:
session.add_all(records)
return [record.case_id for record in records]
class DBToolInput(BaseModel):
"""数据库工具输入参数。"""
testcaselist: List[TestCaseInfo]
# 新建数据库操作工具包
class DBTool(BaseTool):
name: str = "db_tool"
args_schema: Type[BaseModel] = DBToolInput
description: str = "用于将测试用例数据插入到数据库的工具"
def _run(
self,
testcaselist: List[TestCaseInfo | dict[str, Any]],
) -> str:
create_tables()
inserted_ids = insert_test_cases(testcaselist)
return (
f"成功插入 {len(inserted_ids)} 条测试用例,"
f"测试用例ID:{inserted_ids}"
)
models.py文件:
python
# 规范测试用例格式
from typing import List
from pydantic import BaseModel, Field
class TestCaseInfo(BaseModel):
"""测试用例数据模型"""
case_id: str = Field(
...,
description="测试编号,也就是测试用例ID,例如 TC_LOGIN_001"
)
module: str = Field(
...,
description="所属模块,例如 用户登录模块"
)
title: str = Field(
...,
description="测试标题,例如 正确用户名和密码登录"
)
priority: str = Field(
...,
description="测试优先级,例如 P0、P1、P2、P3"
)
precondition: str = Field(
...,
description="执行测试用例之前需要满足的前提条件"
)
steps: List[str] = Field(
...,
description="测试步骤,可以包含多个操作步骤"
)
expected_result: str = Field(
...,
description="执行测试步骤后应该得到的预期结果"
)
actual_result: str = Field(
...,
description="执行测试步骤后实际得到的预期结果"
)
class TestCaseInfoList(BaseModel):
"""测试用例数据模型列表"""
test_case_list: List[TestCaseInfo] = Field(
...,
description="测试用例列表"
)
excel_export.py文件:
python
"""测试用例 Excel 导出模块。"""
from datetime import datetime
from pathlib import Path
from typing import Any, Iterable
from openpyxl import Workbook
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.utils import get_column_letter
from models import TestCaseInfo
HEADERS = [
"测试用例ID",
"所属模块",
"测试标题",
"优先级",
"前置条件",
"测试步骤",
"预期结果",
"实际结果",
]
def _safe_excel_text(value: object) -> str:
"""将值转换为文本,并避免内容被 Excel 识别为公式。"""
text = "" if value is None else str(value)
if text.startswith(("=", "+", "-", "@")):
return f"'{text}"
return text
def export_test_cases(
test_cases: Iterable[TestCaseInfo | dict[str, Any]],
output_path: str | Path | None = None,
) -> str:
"""将测试用例导出为 Excel 文件,并返回文件绝对路径。"""
test_case_list = [
test_case
if isinstance(test_case, TestCaseInfo)
else TestCaseInfo.model_validate(test_case)
for test_case in test_cases
]
if not test_case_list:
raise ValueError("测试用例列表为空,无法导出 Excel")
if output_path is None:
output_directory = Path(__file__).resolve().parent / "exports"
output_directory.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
destination = output_directory / f"测试用例_{timestamp}.xlsx"
else:
destination = Path(output_path).expanduser().resolve()
if destination.suffix.lower() != ".xlsx":
destination = destination.with_suffix(".xlsx")
destination.parent.mkdir(parents=True, exist_ok=True)
workbook = Workbook()
worksheet = workbook.active
worksheet.title = "测试用例"
worksheet.freeze_panes = "A2"
worksheet.sheet_view.showGridLines = False
worksheet.append(HEADERS)
for test_case in test_case_list:
steps = "\n".join(
f"{index}. {_safe_excel_text(step)}"
for index, step in enumerate(test_case.steps, start=1)
)
worksheet.append([
_safe_excel_text(test_case.case_id),
_safe_excel_text(test_case.module),
_safe_excel_text(test_case.title),
_safe_excel_text(test_case.priority),
_safe_excel_text(test_case.precondition),
steps,
_safe_excel_text(test_case.expected_result),
_safe_excel_text(test_case.actual_result),
])
header_fill = PatternFill("solid", fgColor="1F4E78")
header_font = Font(color="FFFFFF", bold=True)
thin_border = Border(bottom=Side(style="thin", color="B7C9D6"))
for cell in worksheet[1]:
cell.fill = header_fill
cell.font = header_font
cell.alignment = Alignment(horizontal="center", vertical="center")
for row in worksheet.iter_rows(min_row=2):
for cell in row:
cell.alignment = Alignment(
horizontal="left",
vertical="top",
wrap_text=True,
)
cell.border = thin_border
column_widths = [18, 20, 28, 10, 32, 45, 38, 38]
for index, width in enumerate(column_widths, start=1):
worksheet.column_dimensions[get_column_letter(index)].width = width
worksheet.row_dimensions[1].height = 24
for row_index, test_case in enumerate(test_case_list, start=2):
worksheet.row_dimensions[row_index].height = max(
36,
18 * max(len(test_case.steps), 1),
)
worksheet.auto_filter.ref = worksheet.dimensions
workbook.save(destination)
return str(destination)
13.2 准备 MySQL
先创建数据库:
sql
CREATE DATABASE IF NOT EXISTS test_platform
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
程序首次入库时会自动创建 test_case_info 表。
13.3 从项目根目录启动
powershell
cd D:\Python\Code\fastApiProject1
.\venv\Scripts\python.exe -m AIAgent.ai_agents
如果仍然直接运行脚本:
powershell
D:\Python\Code\fastApiProject1\venv\Scripts\python.exe D:\Python\Code\fastApiProject1\AIAgent\ai_agents.py
则要保证各文件的导入方式与脚本启动方式一致。长期维护更推荐第一种模块启动方式。
13.4 验证结果
控制台应至少看到:
- 需求分析任务完成;
- 测试用例任务生成结构化结果;
db_tool返回实际插入数量与 ID;- Excel 文件的绝对路径。
运行结果:
-
数据库写入:

-
excel生成:

-
控制台输出:

cmd
D:\Python\Code\fastApiProject1\venv\Scripts\python.exe D:\Python\Code\fastApiProject1\AIAgent\ai_agents.py
┌───────────────────────── 🚀 Crew Execution Started ─────────────────────────┐
│ │
│ Crew Execution Started │
│ Name: crew │
│ ID: 4336200d-76aa-476a-bbce-d608479133a4 │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────── 📋 Task Started ──────────────────────────────┐
│ │
│ Task Started │
│ Name: 分解出详细的需求条目 │
│ ID: cdf35833-6524-42a6-8072-c42ffed7a620 │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────── 🤖 Agent Started ──────────────────────────────┐
│ │
│ Agent: 软件测试需求分析工程师 │
│ │
│ Task: 分解出详细的需求条目 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌────────────────────── 🔧 Tool Execution Started (#1) ───────────────────────┐
│ │
│ Tool: search_a_pdfs_content │
│ Args: {'query': '测试需求 测试要求 测试规范'} │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Tool search_a_pdfs_content executed with result: Relevant Content:
Page 1:
管理员登录接口文档
第 1 页
管理员登录接口文档
1. 基本信息
项目
内容
接口名称
管理员登录(原接口名称:员工登录)
接口描述
管理员使用员工账号登录管理端,登录成功后返回管理员信息和 JWT 令牌。
请求路径
/admin/employee/login
请求方式
POST
Content-Type
application/json
是否...
┌───────────────────── ✅ Tool Execution Completed (#1) ──────────────────────┐
│ │
│ Tool Completed │
│ Tool: search_a_pdfs_content │
│ Output: Relevant Content: │
│ Page 1: │
│ 管理员登录接口文档 │
│ 第 1 页 │
│ 管理员登录接口文档 │
│ 1. 基本信息 │
│ 项目 │
│ 内容 │
│ 接口名称 │
│ 管理员登录(原接口名称:员工登录) │
│ 接口描述 │
│ 管理员使用员工账号登录管理端,登录成功后返回管理员信息和 JWT 令牌。 │
│ 请求路径 │
│ /admin/employee/login │
│ 请求方式 │
│ POST │
│ Content-Type │
│ application/json │
│ 是否需要认证 │
│ 否 │
│ 2. 请求参数 │
│ 2.1 请求头 │
│ 参数名 │
│ 参数值 │
│ 必填 │
│ 说明 │
│ Content-Type │
│ application/json │
│ 是 │
│ 请求体为 JSON 格式 │
│ 2.2 请求体 │
│ 参数名 │
│ 类型 │
│ 必填 │
│ 说明 │
│ 约束 │
│ username │
│ string │
│ 是 │
│ 用户名 │
│ 1-20 位字母或数字,区分大小写,不可为空 │
│ password │
│ string │
│ 是 │
│ 密码 │
│ 6-20 位字母、数字或特殊字符,不可为空 │
│ 2.3 请求示例 │
│ POST /admin/employee/login HTTP/1.1 │
│ Content-Type: application/json │
│ { │
│ "username": "admin", │
│ "password": "123456" │
│ } │
│ │
│ │
│ │
│ │
│ │
│ Page 2: │
│ 管理员登录接口文档 │
│ 第 2 页 │
│ 3. 响应参数 │
│ 字段 │
│ 类型 │
│ 必填 │
│ 说明 │
│ code │
│ integer(int32) │
│ 是 │
│ 业务状态码:1 表示成功,0 表示失败 │
│ data │
│ object │
│ 否 │
│ 登录成功时返回的管理员数据 │
│ data.id │
│ integer(int64) │
│ 否 │
│ 管理员/员工主键 ID │
│ data.name │
│ string │
│ 否 │
│ 姓名 │
│ data.token │
│ string │
│ 否 │
│ JWT 令牌 │
│ data.userName │
│ string │
│ 否 │
│ 用户名 │
│ msg │
│ string │
│ 否 │
│ 提示信息;失败时说明失败原因 │
│ 4. 成功响应示例 │
│ HTTP/1.1 200 OK │
│ Content-Type: application/json │
│ { │
│ "code": 1, │
│ "data": { │
│ "id": 1, │
│ "name": "管理员", │
│ "token": "eyJhbGciOiJIUzI1NiJ9...", │
│ "userName": "admin" │
│ }, │
│ "msg": null │
│ } │
│ 5. 失败响应 │
│ 失败情况 │
│ code │
│ data │
│ msg │
│ 用户名不存在 │
│ 0 │
│ null │
│ 账号不存在 │
│ 密码错误 │
│ 0 │
│ null │
│ 密码错误 │
│ 账号被禁用 │
│ 0 │
│ null │
│ 账号已禁用,请联系管理员 │
│ 请求参数不合法 │
│ 0 │
│ null │
│ 参数校验失败(具体信息以后端返回为准) │
│ 失败响应示例 │
│ { │
│ "code": 0, │
│ "data": null, │
│ "msg": "密码错误" │
│ } │
│ 6. 补充说明 │
│ 1)账号状态正常时才允许登录;账号被禁用时不签发 token。 │
│ 2)附件说明中 JWT 有效期为 2 小时,最终以服务端配置为准。 │
│ 3)后续访问需要认证的管理端接口时,应携带登录返回的 token。 │
│ 4)生产环境应使用 HTTPS,不得在日志中记录明文密码或完整 token。 │
│ 注:原 YApi 导出仅明确了字段结构与必填项;业务状态码、长度限制及 token │
│ 有效期结合附件说明整理,最终以后端实际实现为准。 │
│ │
│ │
│ Page 1: │
│ 管理员登录接口文档 │
│ 第 1 页 │
│ 管理员登录接口文档 │
│ 1. 基本信息 │
│ 项目 │
│ 内容 │
│ 接口名称 │
│ 管理员登录(原接口名称:员工登录) │
│ 接口描述 │
│ 管理员使用员工账号登录管理端,登录成功后返回管理员信息和 JWT 令牌。 │
│ 请求路径 │
│ /admin/employee/login │
│ 请求方式 │
│ POST │
│ Content-Type │
│ application/json │
│ 是否需要认证 │
│ 否 │
│ 2. 请求参数 │
│ 2.1 请求头 │
│ 参数名 │
│ 参数值 │
│ 必填 │
│ 说明 │
│ Content-Type │
│ application/json │
│ 是 │
│ 请求体为 JSON 格式 │
│ 2.2 请求体 │
│ 参数名 │
│ 类型 │
│ 必填 │
│ 说明 │
│ 约束 │
│ username │
│ string │
│ 是 │
│ 用户名 │
│ 1-20 位字母或数字,区分大小写,不可为空 │
│ password │
│ string │
│ 是 │
│ 密码 │
│ 6-20 位字母、数字或特殊字符,不可为空 │
│ 2.3 请求示例 │
│ POST /admin/employee/login HTTP/1.1 │
│ Content-Type: application/json │
│ { │
│ "username": "admin", │
│ "password": "123456" │
│ } │
│ │
│ │
│ │
│ │
│ │
│ Page 2: │
│ 管理员登录接口文档 │
│ 第 2 页 │
│ 3. 响应参数 │
│ 字段 │
│ 类型 │
│ 必填 │
│ 说明 │
│ code │
│ integer(int32) │
│ 是 │
│ 业务状态码:1 表示成功,0 表示失败 │
│ data │
│ object │
│ 否 │
│ 登录成功时返回的管理员数据 │
│ data.id │
│ integer(int64) │
│ 否 │
│ 管理员/员工主键 ID │
│ data.name │
│ string │
│ 否 │
│ 姓名 │
│ data.token │
│ string │
│ 否 │
│ JWT 令牌 │
│ data.userName │
│ string │
│ 否 │
│ 用户名 │
│ msg │
│ string │
│ 否 │
│ 提示信息;失败时说明失败原因 │
│ 4. 成功响应示例 │
│ HTTP/1.1 200 OK │
│ Content-Type: application/json │
│ { │
│ "code": 1, │
│ "data": { │
│ "id": 1, │
│ "name": "管理员", │
│ "token": "eyJhbGciOiJIUzI1NiJ9...", │
│ "userName": "admin" │
│ }, │
│ "msg": null │
│ } │
│ 5. 失败响应 │
│ 失败情况 │
│ code │
│ data │
│ msg │
│ 用户名不存在 │
│ 0 │
│ null │
│ 账号不存在 │
│ 密码错误 │
│ 0 │
│ null │
│ 密码错误 │
│ 账号被禁用 │
│ 0 │
│ null │
│ 账号已禁用,请联系管理员 │
│ 请求参数不合法 │
│ 0 │
│ null │
│ 参数校验失败(具体信息以后端返回为准) │
│ 失败响应示例 │
│ { │
│ "code": 0, │
│ "data": null, │
│ "msg": "密码错误" │
│ } │
│ 6. 补充说明 │
│ 1)账号状态正常时才允许登录;账号被禁用时不签发 token。 │
│ 2)附件说明中 JWT 有效期为 2 小时,最终以服务端配置为准。 │
│ 3)后续访问需要认证的管理端接口时,应携带登录返回的 token。 │
│ 4)生产环境应使用 HTTPS,不得在日志中记录明文密码或完整 token。 │
│ 注:原 YApi 导出仅明确了字段结构与必填项;业务状态码、长度限制及 token │
│ 有效期结合附件说明整理,最终以后端实际实现为准。 │
│ │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌────────────────────── 🔧 Tool Execution Started (#2) ───────────────────────┐
│ │
│ Tool: search_a_pdfs_content │
│ Args: {'query': '用户登录 员工登录 接口文档'} │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Tool search_a_pdfs_content executed with result: Relevant Content:
Page 1:
管理员登录接口文档
第 1 页
管理员登录接口文档
1. 基本信息
项目
内容
接口名称
管理员登录(原接口名称:员工登录)
接口描述
管理员使用员工账号登录管理端,登录成功后返回管理员信息和 JWT 令牌。
请求路径
/admin/employee/login
请求方式
POST
Content-Type
application/json
是否...
┌───────────────────── ✅ Tool Execution Completed (#2) ──────────────────────┐
│ │
│ Tool Completed │
│ Tool: search_a_pdfs_content │
│ Output: Relevant Content: │
│ Page 1: │
│ 管理员登录接口文档 │
│ 第 1 页 │
│ 管理员登录接口文档 │
│ 1. 基本信息 │
│ 项目 │
│ 内容 │
│ 接口名称 │
│ 管理员登录(原接口名称:员工登录) │
│ 接口描述 │
│ 管理员使用员工账号登录管理端,登录成功后返回管理员信息和 JWT 令牌。 │
│ 请求路径 │
│ /admin/employee/login │
│ 请求方式 │
│ POST │
│ Content-Type │
│ application/json │
│ 是否需要认证 │
│ 否 │
│ 2. 请求参数 │
│ 2.1 请求头 │
│ 参数名 │
│ 参数值 │
│ 必填 │
│ 说明 │
│ Content-Type │
│ application/json │
│ 是 │
│ 请求体为 JSON 格式 │
│ 2.2 请求体 │
│ 参数名 │
│ 类型 │
│ 必填 │
│ 说明 │
│ 约束 │
│ username │
│ string │
│ 是 │
│ 用户名 │
│ 1-20 位字母或数字,区分大小写,不可为空 │
│ password │
│ string │
│ 是 │
│ 密码 │
│ 6-20 位字母、数字或特殊字符,不可为空 │
│ 2.3 请求示例 │
│ POST /admin/employee/login HTTP/1.1 │
│ Content-Type: application/json │
│ { │
│ "username": "admin", │
│ "password": "123456" │
│ } │
│ │
│ │
│ │
│ │
│ │
│ Page 2: │
│ 管理员登录接口文档 │
│ 第 2 页 │
│ 3. 响应参数 │
│ 字段 │
│ 类型 │
│ 必填 │
│ 说明 │
│ code │
│ integer(int32) │
│ 是 │
│ 业务状态码:1 表示成功,0 表示失败 │
│ data │
│ object │
│ 否 │
│ 登录成功时返回的管理员数据 │
│ data.id │
│ integer(int64) │
│ 否 │
│ 管理员/员工主键 ID │
│ data.name │
│ string │
│ 否 │
│ 姓名 │
│ data.token │
│ string │
│ 否 │
│ JWT 令牌 │
│ data.userName │
│ string │
│ 否 │
│ 用户名 │
│ msg │
│ string │
│ 否 │
│ 提示信息;失败时说明失败原因 │
│ 4. 成功响应示例 │
│ HTTP/1.1 200 OK │
│ Content-Type: application/json │
│ { │
│ "code": 1, │
│ "data": { │
│ "id": 1, │
│ "name": "管理员", │
│ "token": "eyJhbGciOiJIUzI1NiJ9...", │
│ "userName": "admin" │
│ }, │
│ "msg": null │
│ } │
│ 5. 失败响应 │
│ 失败情况 │
│ code │
│ data │
│ msg │
│ 用户名不存在 │
│ 0 │
│ null │
│ 账号不存在 │
│ 密码错误 │
│ 0 │
│ null │
│ 密码错误 │
│ 账号被禁用 │
│ 0 │
│ null │
│ 账号已禁用,请联系管理员 │
│ 请求参数不合法 │
│ 0 │
│ null │
│ 参数校验失败(具体信息以后端返回为准) │
│ 失败响应示例 │
│ { │
│ "code": 0, │
│ "data": null, │
│ "msg": "密码错误" │
│ } │
│ 6. 补充说明 │
│ 1)账号状态正常时才允许登录;账号被禁用时不签发 token。 │
│ 2)附件说明中 JWT 有效期为 2 小时,最终以服务端配置为准。 │
│ 3)后续访问需要认证的管理端接口时,应携带登录返回的 token。 │
│ 4)生产环境应使用 HTTPS,不得在日志中记录明文密码或完整 token。 │
│ 注:原 YApi 导出仅明确了字段结构与必填项;业务状态码、长度限制及 token │
│ 有效期结合附件说明整理,最终以后端实际实现为准。 │
│ │
│ │
│ Page 1: │
│ 管理员登录接口文档 │
│ 第 1 页 │
│ 管理员登录接口文档 │
│ 1. 基本信息 │
│ 项目 │
│ 内容 │
│ 接口名称 │
│ 管理员登录(原接口名称:员工登录) │
│ 接口描述 │
│ 管理员使用员工账号登录管理端,登录成功后返回管理员信息和 JWT 令牌。 │
│ 请求路径 │
│ /admin/employee/login │
│ 请求方式 │
│ POST │
│ Content-Type │
│ application/json │
│ 是否需要认证 │
│ 否 │
│ 2. 请求参数 │
│ 2.1 请求头 │
│ 参数名 │
│ 参数值 │
│ 必填 │
│ 说明 │
│ Content-Type │
│ application/json │
│ 是 │
│ 请求体为 JSON 格式 │
│ 2.2 请求体 │
│ 参数名 │
│ 类型 │
│ 必填 │
│ 说明 │
│ 约束 │
│ username │
│ string │
│ 是 │
│ 用户名 │
│ 1-20 位字母或数字,区分大小写,不可为空 │
│ password │
│ string │
│ 是 │
│ 密码 │
│ 6-20 位字母、数字或特殊字符,不可为空 │
│ 2.3 请求示例 │
│ POST /admin/employee/login HTTP/1.1 │
│ Content-Type: application/json │
│ { │
│ "username": "admin", │
│ "password": "123456" │
│ } │
│ │
│ │
│ │
│ │
│ │
│ Page 2: │
│ 管理员登录接口文档 │
│ 第 2 页 │
│ 3. 响应参数 │
│ 字段 │
│ 类型 │
│ 必填 │
│ 说明 │
│ code │
│ integer(int32) │
│ 是 │
│ 业务状态码:1 表示成功,0 表示失败 │
│ data │
│ object │
│ 否 │
│ 登录成功时返回的管理员数据 │
│ data.id │
│ integer(int64) │
│ 否 │
│ 管理员/员工主键 ID │
│ data.name │
│ string │
│ 否 │
│ 姓名 │
│ data.token │
│ string │
│ 否 │
│ JWT 令牌 │
│ data.userName │
│ string │
│ 否 │
│ 用户名 │
│ msg │
│ string │
│ 否 │
│ 提示信息;失败时说明失败原因 │
│ 4. 成功响应示例 │
│ HTTP/1.1 200 OK │
│ Content-Type: application/json │
│ { │
│ "code": 1, │
│ "data": { │
│ "id": 1, │
│ "name": "管理员", │
│ "token": "eyJhbGciOiJIUzI1NiJ9...", │
│ "userName": "admin" │
│ }, │
│ "msg": null │
│ } │
│ 5. 失败响应 │
│ 失败情况 │
│ code │
│ data │
│ msg │
│ 用户名不存在 │
│ 0 │
│ null │
│ 账号不存在 │
│ 密码错误 │
│ 0 │
│ null │
│ 密码错误 │
│ 账号被禁用 │
│ 0 │
│ null │
│ 账号已禁用,请联系管理员 │
│ 请求参数不合法 │
│ 0 │
│ null │
│ 参数校验失败(具体信息以后端返回为准) │
│ 失败响应示例 │
│ { │
│ "code": 0, │
│ "data": null, │
│ "msg": "密码错误" │
│ } │
│ 6. 补充说明 │
│ 1)账号状态正常时才允许登录;账号被禁用时不签发 token。 │
│ 2)附件说明中 JWT 有效期为 2 小时,最终以服务端配置为准。 │
│ 3)后续访问需要认证的管理端接口时,应携带登录返回的 token。 │
│ 4)生产环境应使用 HTTPS,不得在日志中记录明文密码或完整 token。 │
│ 注:原 YApi 导出仅明确了字段结构与必填项;业务状态码、长度限制及 token │
│ 有效期结合附件说明整理,最终以后端实际实现为准。 │
│ │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌────────────────────── 🔧 Tool Execution Started (#3) ───────────────────────┐
│ │
│ Tool: search_a_pdfs_content │
│ Args: {'query': '参数校验 约束条件 长度限制'} │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Tool search_a_pdfs_content executed with result: Relevant Content:
Page 1:
管理员登录接口文档
第 1 页
管理员登录接口文档
1. 基本信息
项目
内容
接口名称
管理员登录(原接口名称:员工登录)
接口描述
管理员使用员工账号登录管理端,登录成功后返回管理员信息和 JWT 令牌。
请求路径
/admin/employee/login
请求方式
POST
Content-Type
application/json
是否...
┌───────────────────── ✅ Tool Execution Completed (#3) ──────────────────────┐
│ │
│ Tool Completed │
│ Tool: search_a_pdfs_content │
│ Output: Relevant Content: │
│ Page 1: │
│ 管理员登录接口文档 │
│ 第 1 页 │
│ 管理员登录接口文档 │
│ 1. 基本信息 │
│ 项目 │
│ 内容 │
│ 接口名称 │
│ 管理员登录(原接口名称:员工登录) │
│ 接口描述 │
│ 管理员使用员工账号登录管理端,登录成功后返回管理员信息和 JWT 令牌。 │
│ 请求路径 │
│ /admin/employee/login │
│ 请求方式 │
│ POST │
│ Content-Type │
│ application/json │
│ 是否需要认证 │
│ 否 │
│ 2. 请求参数 │
│ 2.1 请求头 │
│ 参数名 │
│ 参数值 │
│ 必填 │
│ 说明 │
│ Content-Type │
│ application/json │
│ 是 │
│ 请求体为 JSON 格式 │
│ 2.2 请求体 │
│ 参数名 │
│ 类型 │
│ 必填 │
│ 说明 │
│ 约束 │
│ username │
│ string │
│ 是 │
│ 用户名 │
│ 1-20 位字母或数字,区分大小写,不可为空 │
│ password │
│ string │
│ 是 │
│ 密码 │
│ 6-20 位字母、数字或特殊字符,不可为空 │
│ 2.3 请求示例 │
│ POST /admin/employee/login HTTP/1.1 │
│ Content-Type: application/json │
│ { │
│ "username": "admin", │
│ "password": "123456" │
│ } │
│ │
│ │
│ │
│ │
│ │
│ Page 2: │
│ 管理员登录接口文档 │
│ 第 2 页 │
│ 3. 响应参数 │
│ 字段 │
│ 类型 │
│ 必填 │
│ 说明 │
│ code │
│ integer(int32) │
│ 是 │
│ 业务状态码:1 表示成功,0 表示失败 │
│ data │
│ object │
│ 否 │
│ 登录成功时返回的管理员数据 │
│ data.id │
│ integer(int64) │
│ 否 │
│ 管理员/员工主键 ID │
│ data.name │
│ string │
│ 否 │
│ 姓名 │
│ data.token │
│ string │
│ 否 │
│ JWT 令牌 │
│ data.userName │
│ string │
│ 否 │
│ 用户名 │
│ msg │
│ string │
│ 否 │
│ 提示信息;失败时说明失败原因 │
│ 4. 成功响应示例 │
│ HTTP/1.1 200 OK │
│ Content-Type: application/json │
│ { │
│ "code": 1, │
│ "data": { │
│ "id": 1, │
│ "name": "管理员", │
│ "token": "eyJhbGciOiJIUzI1NiJ9...", │
│ "userName": "admin" │
│ }, │
│ "msg": null │
│ } │
│ 5. 失败响应 │
│ 失败情况 │
│ code │
│ data │
│ msg │
│ 用户名不存在 │
│ 0 │
│ null │
│ 账号不存在 │
│ 密码错误 │
│ 0 │
│ null │
│ 密码错误 │
│ 账号被禁用 │
│ 0 │
│ null │
│ 账号已禁用,请联系管理员 │
│ 请求参数不合法 │
│ 0 │
│ null │
│ 参数校验失败(具体信息以后端返回为准) │
│ 失败响应示例 │
│ { │
│ "code": 0, │
│ "data": null, │
│ "msg": "密码错误" │
│ } │
│ 6. 补充说明 │
│ 1)账号状态正常时才允许登录;账号被禁用时不签发 token。 │
│ 2)附件说明中 JWT 有效期为 2 小时,最终以服务端配置为准。 │
│ 3)后续访问需要认证的管理端接口时,应携带登录返回的 token。 │
│ 4)生产环境应使用 HTTPS,不得在日志中记录明文密码或完整 token。 │
│ 注:原 YApi 导出仅明确了字段结构与必填项;业务状态码、长度限制及 token │
│ 有效期结合附件说明整理,最终以后端实际实现为准。 │
│ │
│ │
│ Page 1: │
│ 管理员登录接口文档 │
│ 第 1 页 │
│ 管理员登录接口文档 │
│ 1. 基本信息 │
│ 项目 │
│ 内容 │
│ 接口名称 │
│ 管理员登录(原接口名称:员工登录) │
│ 接口描述 │
│ 管理员使用员工账号登录管理端,登录成功后返回管理员信息和 JWT 令牌。 │
│ 请求路径 │
│ /admin/employee/login │
│ 请求方式 │
│ POST │
│ Content-Type │
│ application/json │
│ 是否需要认证 │
│ 否 │
│ 2. 请求参数 │
│ 2.1 请求头 │
│ 参数名 │
│ 参数值 │
│ 必填 │
│ 说明 │
│ Content-Type │
│ application/json │
│ 是 │
│ 请求体为 JSON 格式 │
│ 2.2 请求体 │
│ 参数名 │
│ 类型 │
│ 必填 │
│ 说明 │
│ 约束 │
│ username │
│ string │
│ 是 │
│ 用户名 │
│ 1-20 位字母或数字,区分大小写,不可为空 │
│ password │
│ string │
│ 是 │
│ 密码 │
│ 6-20 位字母、数字或特殊字符,不可为空 │
│ 2.3 请求示例 │
│ POST /admin/employee/login HTTP/1.1 │
│ Content-Type: application/json │
│ { │
│ "username": "admin", │
│ "password": "123456" │
│ } │
│ │
│ │
│ │
│ │
│ │
│ Page 2: │
│ 管理员登录接口文档 │
│ 第 2 页 │
│ 3. 响应参数 │
│ 字段 │
│ 类型 │
│ 必填 │
│ 说明 │
│ code │
│ integer(int32) │
│ 是 │
│ 业务状态码:1 表示成功,0 表示失败 │
│ data │
│ object │
│ 否 │
│ 登录成功时返回的管理员数据 │
│ data.id │
│ integer(int64) │
│ 否 │
│ 管理员/员工主键 ID │
│ data.name │
│ string │
│ 否 │
│ 姓名 │
│ data.token │
│ string │
│ 否 │
│ JWT 令牌 │
│ data.userName │
│ string │
│ 否 │
│ 用户名 │
│ msg │
│ string │
│ 否 │
│ 提示信息;失败时说明失败原因 │
│ 4. 成功响应示例 │
│ HTTP/1.1 200 OK │
│ Content-Type: application/json │
│ { │
│ "code": 1, │
│ "data": { │
│ "id": 1, │
│ "name": "管理员", │
│ "token": "eyJhbGciOiJIUzI1NiJ9...", │
│ "userName": "admin" │
│ }, │
│ "msg": null │
│ } │
│ 5. 失败响应 │
│ 失败情况 │
│ code │
│ data │
│ msg │
│ 用户名不存在 │
│ 0 │
│ null │
│ 账号不存在 │
│ 密码错误 │
│ 0 │
│ null │
│ 密码错误 │
│ 账号被禁用 │
│ 0 │
│ null │
│ 账号已禁用,请联系管理员 │
│ 请求参数不合法 │
│ 0 │
│ null │
│ 参数校验失败(具体信息以后端返回为准) │
│ 失败响应示例 │
│ { │
│ "code": 0, │
│ "data": null, │
│ "msg": "密码错误" │
│ } │
│ 6. 补充说明 │
│ 1)账号状态正常时才允许登录;账号被禁用时不签发 token。 │
│ 2)附件说明中 JWT 有效期为 2 小时,最终以服务端配置为准。 │
│ 3)后续访问需要认证的管理端接口时,应携带登录返回的 token。 │
│ 4)生产环境应使用 HTTPS,不得在日志中记录明文密码或完整 token。 │
│ 注:原 YApi 导出仅明确了字段结构与必填项;业务状态码、长度限制及 token │
│ 有效期结合附件说明整理,最终以后端实际实现为准。 │
│ │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌────────────────────── 🔧 Tool Execution Started (#4) ───────────────────────┐
│ │
│ Tool: search_a_pdfs_content │
│ Args: {'query': '失败响应 错误码 错误信息'} │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Tool search_a_pdfs_content executed with result: Relevant Content:
Page 1:
管理员登录接口文档
第 1 页
管理员登录接口文档
1. 基本信息
项目
内容
接口名称
管理员登录(原接口名称:员工登录)
接口描述
管理员使用员工账号登录管理端,登录成功后返回管理员信息和 JWT 令牌。
请求路径
/admin/employee/login
请求方式
POST
Content-Type
application/json
是否...
┌───────────────────── ✅ Tool Execution Completed (#4) ──────────────────────┐
│ │
│ Tool Completed │
│ Tool: search_a_pdfs_content │
│ Output: Relevant Content: │
│ Page 1: │
│ 管理员登录接口文档 │
│ 第 1 页 │
│ 管理员登录接口文档 │
│ 1. 基本信息 │
│ 项目 │
│ 内容 │
│ 接口名称 │
│ 管理员登录(原接口名称:员工登录) │
│ 接口描述 │
│ 管理员使用员工账号登录管理端,登录成功后返回管理员信息和 JWT 令牌。 │
│ 请求路径 │
│ /admin/employee/login │
│ 请求方式 │
│ POST │
│ Content-Type │
│ application/json │
│ 是否需要认证 │
│ 否 │
│ 2. 请求参数 │
│ 2.1 请求头 │
│ 参数名 │
│ 参数值 │
│ 必填 │
│ 说明 │
│ Content-Type │
│ application/json │
│ 是 │
│ 请求体为 JSON 格式 │
│ 2.2 请求体 │
│ 参数名 │
│ 类型 │
│ 必填 │
│ 说明 │
│ 约束 │
│ username │
│ string │
│ 是 │
│ 用户名 │
│ 1-20 位字母或数字,区分大小写,不可为空 │
│ password │
│ string │
│ 是 │
│ 密码 │
│ 6-20 位字母、数字或特殊字符,不可为空 │
│ 2.3 请求示例 │
│ POST /admin/employee/login HTTP/1.1 │
│ Content-Type: application/json │
│ { │
│ "username": "admin", │
│ "password": "123456" │
│ } │
│ │
│ │
│ │
│ │
│ │
│ Page 2: │
│ 管理员登录接口文档 │
│ 第 2 页 │
│ 3. 响应参数 │
│ 字段 │
│ 类型 │
│ 必填 │
│ 说明 │
│ code │
│ integer(int32) │
│ 是 │
│ 业务状态码:1 表示成功,0 表示失败 │
│ data │
│ object │
│ 否 │
│ 登录成功时返回的管理员数据 │
│ data.id │
│ integer(int64) │
│ 否 │
│ 管理员/员工主键 ID │
│ data.name │
│ string │
│ 否 │
│ 姓名 │
│ data.token │
│ string │
│ 否 │
│ JWT 令牌 │
│ data.userName │
│ string │
│ 否 │
│ 用户名 │
│ msg │
│ string │
│ 否 │
│ 提示信息;失败时说明失败原因 │
│ 4. 成功响应示例 │
│ HTTP/1.1 200 OK │
│ Content-Type: application/json │
│ { │
│ "code": 1, │
│ "data": { │
│ "id": 1, │
│ "name": "管理员", │
│ "token": "eyJhbGciOiJIUzI1NiJ9...", │
│ "userName": "admin" │
│ }, │
│ "msg": null │
│ } │
│ 5. 失败响应 │
│ 失败情况 │
│ code │
│ data │
│ msg │
│ 用户名不存在 │
│ 0 │
│ null │
│ 账号不存在 │
│ 密码错误 │
│ 0 │
│ null │
│ 密码错误 │
│ 账号被禁用 │
│ 0 │
│ null │
│ 账号已禁用,请联系管理员 │
│ 请求参数不合法 │
│ 0 │
│ null │
│ 参数校验失败(具体信息以后端返回为准) │
│ 失败响应示例 │
│ { │
│ "code": 0, │
│ "data": null, │
│ "msg": "密码错误" │
│ } │
│ 6. 补充说明 │
│ 1)账号状态正常时才允许登录;账号被禁用时不签发 token。 │
│ 2)附件说明中 JWT 有效期为 2 小时,最终以服务端配置为准。 │
│ 3)后续访问需要认证的管理端接口时,应携带登录返回的 token。 │
│ 4)生产环境应使用 HTTPS,不得在日志中记录明文密码或完整 token。 │
│ 注:原 YApi 导出仅明确了字段结构与必填项;业务状态码、长度限制及 token │
│ 有效期结合附件说明整理,最终以后端实际实现为准。 │
│ │
│ │
│ Page 1: │
│ 管理员登录接口文档 │
│ 第 1 页 │
│ 管理员登录接口文档 │
│ 1. 基本信息 │
│ 项目 │
│ 内容 │
│ 接口名称 │
│ 管理员登录(原接口名称:员工登录) │
│ 接口描述 │
│ 管理员使用员工账号登录管理端,登录成功后返回管理员信息和 JWT 令牌。 │
│ 请求路径 │
│ /admin/employee/login │
│ 请求方式 │
│ POST │
│ Content-Type │
│ application/json │
│ 是否需要认证 │
│ 否 │
│ 2. 请求参数 │
│ 2.1 请求头 │
│ 参数名 │
│ 参数值 │
│ 必填 │
│ 说明 │
│ Content-Type │
│ application/json │
│ 是 │
│ 请求体为 JSON 格式 │
│ 2.2 请求体 │
│ 参数名 │
│ 类型 │
│ 必填 │
│ 说明 │
│ 约束 │
│ username │
│ string │
│ 是 │
│ 用户名 │
│ 1-20 位字母或数字,区分大小写,不可为空 │
│ password │
│ string │
│ 是 │
│ 密码 │
│ 6-20 位字母、数字或特殊字符,不可为空 │
│ 2.3 请求示例 │
│ POST /admin/employee/login HTTP/1.1 │
│ Content-Type: application/json │
│ { │
│ "username": "admin", │
│ "password": "123456" │
│ } │
│ │
│ │
│ │
│ │
│ │
│ Page 2: │
│ 管理员登录接口文档 │
│ 第 2 页 │
│ 3. 响应参数 │
│ 字段 │
│ 类型 │
│ 必填 │
│ 说明 │
│ code │
│ integer(int32) │
│ 是 │
│ 业务状态码:1 表示成功,0 表示失败 │
│ data │
│ object │
│ 否 │
│ 登录成功时返回的管理员数据 │
│ data.id │
│ integer(int64) │
│ 否 │
│ 管理员/员工主键 ID │
│ data.name │
│ string │
│ 否 │
│ 姓名 │
│ data.token │
│ string │
│ 否 │
│ JWT 令牌 │
│ data.userName │
│ string │
│ 否 │
│ 用户名 │
│ msg │
│ string │
│ 否 │
│ 提示信息;失败时说明失败原因 │
│ 4. 成功响应示例 │
│ HTTP/1.1 200 OK │
│ Content-Type: application/json │
│ { │
│ "code": 1, │
│ "data": { │
│ "id": 1, │
│ "name": "管理员", │
│ "token": "eyJhbGciOiJIUzI1NiJ9...", │
│ "userName": "admin" │
│ }, │
│ "msg": null │
│ } │
│ 5. 失败响应 │
│ 失败情况 │
│ code │
│ data │
│ msg │
│ 用户名不存在 │
│ 0 │
│ null │
│ 账号不存在 │
│ 密码错误 │
│ 0 │
│ null │
│ 密码错误 │
│ 账号被禁用 │
│ 0 │
│ null │
│ 账号已禁用,请联系管理员 │
│ 请求参数不合法 │
│ 0 │
│ null │
│ 参数校验失败(具体信息以后端返回为准) │
│ 失败响应示例 │
│ { │
│ "code": 0, │
│ "data": null, │
│ "msg": "密码错误" │
│ } │
│ 6. 补充说明 │
│ 1)账号状态正常时才允许登录;账号被禁用时不签发 token。 │
│ 2)附件说明中 JWT 有效期为 2 小时,最终以服务端配置为准。 │
│ 3)后续访问需要认证的管理端接口时,应携带登录返回的 token。 │
│ 4)生产环境应使用 HTTPS,不得在日志中记录明文密码或完整 token。 │
│ 注:原 YApi 导出仅明确了字段结构与必填项;业务状态码、长度限制及 token │
│ 有效期结合附件说明整理,最终以后端实际实现为准。 │
│ │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
[Finalize] todos_count=0, todos_with_results=0
┌─────────────────────────── ✅ Agent Final Answer ───────────────────────────┐
│ │
│ Agent: 软件测试需求分析工程师 │
│ │
│ Final Answer: │
│ 基于对PDF文件的分析,我已经获取了管理员登录接口的详细信息。现在我将分解出 │
│ 详细的测试需求条目: │
│ │
│ # 管理员登录接口详细测试需求条目 │
│ │
│ ## 一、接口基本信息测试需求 │
│ │
│ ### 1. 接口基础信息验证需求 │
│ - **REQ-001**: 验证接口名称是否为"管理员登录"(原接口名称为"员工登录") │
│ - **REQ-002**: │
│ 验证接口描述应准确描述:管理员使用员工账号登录管理端,登录成功后返回管理 │
│ 员信息和JWT令牌 │
│ - **REQ-003**: 验证请求路径为:/admin/employee/login │
│ - **REQ-004**: 验证请求方式为:POST │
│ - **REQ-005**: 验证Content-Type为:application/json │
│ - **REQ-006**: 验证该接口是否需要认证:否 │
│ │
│ ## 二、请求参数测试需求 │
│ │
│ ### 2. 请求头参数测试需求 │
│ - **REQ-007**: 验证请求头中Content-Type参数值为application/json │
│ - **REQ-008**: 验证Content-Type参数为必填项 │
│ - **REQ-009**: 验证请求体必须为JSON格式 │
│ │
│ ### 3. 请求体参数测试需求 │
│ │
│ #### 用户名(username)参数测试需求: │
│ - **REQ-010**: 验证username参数类型为string │
│ - **REQ-011**: 验证username参数为必填项 │
│ - **REQ-012**: 验证username长度约束:1-20位字符 │
│ - **REQ-013**: 验证username字符格式约束:仅允许字母或数字 │
│ - **REQ-014**: 验证username区分大小写 │
│ - **REQ-015**: 验证username不可为空 │
│ │
│ #### 密码(password)参数测试需求: │
│ - **REQ-016**: 验证password参数类型为string │
│ - **REQ-017**: 验证password参数为必填项 │
│ - **REQ-018**: 验证password长度约束:6-20位字符 │
│ - **REQ-019**: 验证password字符格式约束:允许字母、数字或特殊字符 │
│ - **REQ-020**: 验证password不可为空 │
│ │
│ ### 4. 请求示例验证需求 │
│ - **REQ-021**: 验证请求示例格式正确性 │
│ - **REQ-022**: 验证请求示例中的参数值符合约束条件 │
│ │
│ ## 三、响应参数测试需求 │
│ │
│ ### 5. 响应结构测试需求 │
│ - **REQ-023**: 验证响应中包含code字段,类型为integer(int32),必填 │
│ - **REQ-024**: 验证响应中包含data字段,类型为object,非必填 │
│ - **REQ-025**: 验证响应中包含msg字段,类型为string,非必填 │
│ │
│ ### 6. 成功响应数据字段测试需求 │
│ - **REQ-026**: 验证成功时data.id字段类型为integer(int64) │
│ - **REQ-027**: 验证成功时data.name字段类型为string │
│ - **REQ-028**: 验证成功时data.token字段类型为string │
│ - **REQ-029**: 验证成功时data.userName字段类型为string │
│ │
│ ### 7. 业务状态码测试需求 │
│ - **REQ-030**: 验证code为1表示登录成功 │
│ - **REQ-031**: 验证code为0表示登录失败 │
│ │
│ ## 四、成功场景测试需求 │
│ │
│ ### 8. 正常登录成功测试需求 │
│ - **REQ-032**: 使用正确的用户名和密码进行登录,验证返回code为1 │
│ - **REQ-033**: 验证登录成功后返回的data字段不为null │
│ - **REQ-034**: 验证登录成功后返回的data.id不为空 │
│ - **REQ-035**: 验证登录成功后返回的data.name不为空 │
│ - **REQ-036**: 验证登录成功后返回的data.token不为空且为有效的JWT格式 │
│ - **REQ-037**: 验证登录成功后返回的data.userName与请求的用户名一致 │
│ - **REQ-038**: 验证登录成功后返回的msg字段为null或空 │
│ │
│ ### 9. 成功响应示例验证需求 │
│ - **REQ-039**: 验证成功响应示例格式正确性 │
│ - **REQ-040**: 验证成功响应示例中的字段类型和值符合规范 │
│ │
│ ## 五、失败场景测试需求 │
│ │
│ ### 10. 用户名不存在测试需求 │
│ - **REQ-041**: 使用不存在的用户名进行登录,验证返回code为0 │
│ - **REQ-042**: 验证失败时data字段为null │
│ - **REQ-043**: 验证失败时msg字段为"账号不存在" │
│ │
│ ### 11. 密码错误测试需求 │
│ - **REQ-044**: 使用错误密码进行登录,验证返回code为0 │
│ - **REQ-045**: 验证失败时data字段为null │
│ - **REQ-046**: 验证失败时msg字段为"密码错误" │
│ │
│ ### 12. 账号被禁用测试需求 │
│ - **REQ-047**: 使用被禁用的账号进行登录,验证返回code为0 │
│ - **REQ-048**: 验证失败时data字段为null │
│ - **REQ-049**: 验证失败时msg字段为"账号已禁用,请联系管理员" │
│ │
│ ### 13. 请求参数不合法测试需求 │
│ - **REQ-050**: 验证参数校验失败时返回code为0 │
│ - **REQ-051**: 验证参数校验失败时data字段为null │
│ - **REQ-052**: │
│ 验证参数校验失败时msg字段为"参数校验失败"(具体信息以后端返回为准) │
│ │
│ ### 14. 失败响应示例验证需求 │
│ - **REQ-053**: 验证失败响应示例格式正确性 │
│ │
│ ## 六、边界值测试需求 │
│ │
│ ### 15. 用户名边界值测试需求 │
│ - **REQ-054**: 验证用户名为空字符串时登录失败 │
│ - **REQ-055**: 验证用户名为1个字符(符合约束)时登录成功 │
│ - **REQ-056**: 验证用户名为20个字符(符合约束)时登录成功 │
│ - **REQ-057**: 验证用户名为21个字符(超出约束)时参数校验失败 │
│ - **REQ-058**: 验证用户名包含特殊字符时参数校验失败 │
│ - **REQ-059**: 验证用户名包含中文时参数校验失败 │
│ - **REQ-060**: 验证用户名包含空格时参数校验失败 │
│ │
│ ### 16. 密码边界值测试需求 │
│ - **REQ-061**: 验证密码为空字符串时登录失败 │
│ - **REQ-062**: 验证密码为6个字符(符合约束)时登录成功 │
│ - **REQ-063**: 验证密码为20个字符(符合约束)时登录成功 │
│ - **REQ-064**: 验证密码为5个字符(小于约束)时参数校验失败 │
│ - **REQ-065**: 验证密码为21个字符(超出约束)时参数校验失败 │
│ - **REQ-066**: 验证密码包含各种允许的特殊字符时登录成功 │
│ │
│ ## 七、特殊场景测试需求 │
│ │
│ ### 17. 请求头异常测试需求 │
│ - **REQ-067**: 验证缺少Content-Type请求头时接口处理 │
│ - **REQ-068**: 验证Content-Type为其他格式(如text/plain)时接口处理 │
│ - **REQ-069**: 验证额外请求头参数时接口处理 │
│ │
│ ### 18. 请求体异常测试需求 │
│ - **REQ-070**: 验证请求体为空时的接口响应 │
│ - **REQ-071**: 验证请求体为非JSON格式时的接口响应 │
│ - **REQ-072**: 验证请求体缺少username字段时的接口响应 │
│ - **REQ-073**: 验证请求体缺少password字段时的接口响应 │
│ - **REQ-074**: 验证请求体包含额外字段时的接口处理 │
│ │
│ ### 19. 请求方法异常测试需求 │
│ - **REQ-075**: 验证使用GET方法请求接口时的响应 │
│ - **REQ-076**: 验证使用PUT方法请求接口时的响应 │
│ - **REQ-077**: 验证使用DELETE方法请求接口时的响应 │
│ │
│ ## 八、安全相关测试需求 │
│ │
│ ### 20. 账号状态验证测试需求 │
│ - **REQ-078**: 验证只有账号状态正常时才允许登录 │
│ - **REQ-079**: 验证账号被禁用时不签发token │
│ │
│ ### 21. JWT令牌测试需求 │
│ - **REQ-080**: 验证登录成功后返回的token为有效的JWT格式 │
│ - **REQ-081**: 验证JWT有效期为2小时(最终以服务端配置为准) │
│ - **REQ-082**: 验证token可用于后续需要认证的管理端接口 │
│ │
│ ### 22. 安全规范测试需求 │
│ - **REQ-083**: 验证生产环境应使用HTTPS协议 │
│ - **REQ-084**: 验证不得在日志中记录明文密码 │
│ - **REQ-085**: 验证不得在日志中记录完整token │
│ │
│ ## 九、性能测试需求 │
│ │
│ ### 23. 响应时间测试需求 │
│ - **REQ-086**: 验证接口正常登录场景的响应时间在合理范围内 │
│ - **REQ-087**: 验证接口失败场景的响应时间在合理范围内 │
│ │
│ ### 24. 并发测试需求 │
│ - **REQ-088**: 验证接口在高并发登录场景下的稳定性 │
│ - **REQ-089**: 验证接口在并发登录时的正确性 │
│ │
│ ## 十、兼容性测试需求 │
│ │
│ ### 25. 数据格式兼容性测试需求 │
│ - **REQ-090**: 验证不同JSON解析库对请求体的兼容性 │
│ - **REQ-091**: 验证不同字符编码对参数的兼容性 │
│ │
│ ### 26. 客户端兼容性测试需求 │
│ - **REQ-092**: 验证不同HTTP客户端对接口的兼容性 │
│ │
│ ## 十一、文档一致性测试需求 │
│ │
│ ### 27. 实现与文档一致性测试需求 │
│ - **REQ-093**: 验证实际接口实现与文档描述的一致性 │
│ - **REQ-094**: 验证业务状态码实际值与文档描述的一致性 │
│ - **REQ-095**: 验证长度限制实际值与文档描述的一致性 │
│ - **REQ-096**: 验证token有效期实际值与文档描述的一致性 │
│ - **REQ-097**: 验证错误信息实际值与文档描述的一致性 │
│ │
│ ## 十二、备注说明相关测试需求 │
│ │
│ ### 28. 补充说明验证需求 │
│ - **REQ-098**: 验证备注说明中各项要求在实际接口中的实现情况 │
│ │
│ 以上共计98条详细的测试需求条目,涵盖了管理员登录接口的功能、性能、安全、 │
│ 兼容性等各个方面,为全面的测试执行提供了详细的指导依据。 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────── 📋 Task Completion ─────────────────────────────┐
│ │
│ Task Completed │
│ Name: 分解出详细的需求条目 │
│ Agent: 软件测试需求分析工程师 │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────── 📋 Task Started ──────────────────────────────┐
│ │
│ Task Started │
│ Name: │
│ 根据需求列表,编写测试用例,尽可能覆盖到多种用例设计方法,输出语言为中文。 │
│ 每条测试用例的属性信息如下: │
│ case_id: str = │
│ Field(...,description="测试编号,也就是测试用例ID,例如 TC_LOGIN_001") │
│ module: str = Field(...,description="所属模块,例如 用户登录模块") │
│ title: str = Field(...,description="测试标题,例如 │
│ 正确用户名和密码登录") │
│ priority: str = Field(...,description="测试优先级,例如 │
│ P0、P1、P2、P3") │
│ precondition: str = │
│ Field(...,description="执行测试用例之前需要满足的前提条件") │
│ steps: List[str] = │
│ Field(...,description="测试步骤,可以包含多个操作步骤") │
│ expected_result: str = │
│ Field(...,description="执行测试步骤后应该得到的预期结果") │
│ actual_result: str = Field(..., │
│ description="执行测试步骤后实际得到的预期结果") │
│ ID: b47485fb-18b7-41fe-b310-75fcbcfc8ada │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────── 🤖 Agent Started ──────────────────────────────┐
│ │
│ Agent: 软件测试用例编写工程师 │
│ │
│ Task: │
│ 根据需求列表,编写测试用例,尽可能覆盖到多种用例设计方法,输出语言为中文。 │
│ 每条测试用例的属性信息如下: │
│ case_id: str = │
│ Field(...,description="测试编号,也就是测试用例ID,例如 TC_LOGIN_001") │
│ module: str = Field(...,description="所属模块,例如 用户登录模块") │
│ title: str = Field(...,description="测试标题,例如 │
│ 正确用户名和密码登录") │
│ priority: str = Field(...,description="测试优先级,例如 │
│ P0、P1、P2、P3") │
│ precondition: str = │
│ Field(...,description="执行测试用例之前需要满足的前提条件") │
│ steps: List[str] = │
│ Field(...,description="测试步骤,可以包含多个操作步骤") │
│ expected_result: str = │
│ Field(...,description="执行测试步骤后应该得到的预期结果") │
│ actual_result: str = Field(..., │
│ description="执行测试步骤后实际得到的预期结果") │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
[Finalize] todos_count=0, todos_with_results=0
┌─────────────────────────── ✅ Agent Final Answer ───────────────────────────┐
│ │
│ Agent: 软件测试用例编写工程师 │
│ │
│ Final Answer: │
│ test_case_list=[TestCaseInfo(case_id='TC_BASIC_001', │
│ module='接口基本信息', title='验证接口名称', priority='P1', │
│ precondition='拥有接口文档或访问接口元信息的权限', │
│ steps=['访问接口文档或调用获取接口信息的接口'], │
│ expected_result='接口名称显示为"管理员登录"', │
│ actual_result='接口名称为:管理员登录'), │
│ TestCaseInfo(case_id='TC_BASIC_002', module='接口基本信息', │
│ title='验证接口描述', priority='P1', │
│ precondition='拥有接口文档或访问接口元信息的权限', │
│ steps=['访问接口文档或调用获取接口信息的接口'], │
│ expected_result='接口描述准确无误', │
│ actual_result='接口描述为:管理员使用员工账号登录管理端,登录成功后返回管 │
│ 理员信息和JWT令牌'), TestCaseInfo(case_id='TC_BASIC_003', │
│ module='接口基本信息', title='验证请求路径', priority='P0', │
│ precondition='拥有接口文档或访问接口元信息的权限', │
│ steps=['访问接口文档或调用获取接口信息的接口'], │
│ expected_result='接口请求路径正确', │
│ actual_result='请求路径为:/admin/employee/login'), │
│ TestCaseInfo(case_id='TC_BASIC_004', module='接口基本信息', │
│ title='验证请求方法', priority='P0', │
│ precondition='拥有接口文档或访问接口元信息的权限', │
│ steps=['访问接口文档或调用获取接口信息的接口'], │
│ expected_result='接口请求方法为POST', actual_result='请求方式为:POST'), │
│ TestCaseInfo(case_id='TC_BASIC_005', module='接口基本信息', │
│ title='验证Content-Type', priority='P1', │
│ precondition='拥有接口文档或访问接口元信息的权限', │
│ steps=['访问接口文档或调用获取接口信息的接口'], │
│ expected_result='接口Content-Type为application/json', │
│ actual_result='Content-Type为:application/json'), │
│ TestCaseInfo(case_id='TC_BASIC_006', module='接口基本信息', │
│ title='验证接口是否需要认证', priority='P1', │
│ precondition='拥有接口文档或访问接口元信息的权限', │
│ steps=['访问接口文档或调用获取接口信息的接口'], │
│ expected_result='接口无需认证即可访问', │
│ actual_result='接口认证要求为:否'), │
│ TestCaseInfo(case_id='TC_REQ_HEADER_001', module='请求参数', │
│ title='验证请求头Content-Type为application/json', priority='P1', │
│ precondition='准备好有效的用户名和密码;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', │
│ '请求体为合法的JSON格式,包含正确的username和password字段', '发送请求'], │
│ expected_result='接口正常处理请求并返回登录结果', │
│ actual_result='请求成功并返回正确响应'), │
│ TestCaseInfo(case_id='TC_REQ_HEADER_002', module='请求参数', │
│ title='验证缺少Content-Type请求头', priority='P1', │
│ precondition='准备好有效的用户名和密码;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', │
│ '不设置Content-Type请求头', │
│ '请求体为合法的JSON格式,包含正确的username和password字段', '发送请求'], │
│ expected_result='接口应拒绝处理或返回明确的错误信息', │
│ actual_result='接口返回状态码400或提示缺少Content-Type'), │
│ TestCaseInfo(case_id='TC_REQ_HEADER_003', module='请求参数', │
│ title='验证Content-Type为其他格式(如text/plain)', priority='P1', │
│ precondition='准备好有效的用户名和密码;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: text/plain', │
│ '请求体为合法的JSON格式,包含正确的username和password字段', '发送请求'], │
│ expected_result='接口应拒绝处理或返回明确的错误信息', │
│ actual_result='接口返回状态码400或415,提示不支持的内容类型'), │
│ TestCaseInfo(case_id='TC_REQ_BODY_001', module='请求参数', │
│ title='验证请求体为合法的JSON格式', priority='P0', │
│ precondition='准备好有效的用户名和密码;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "validUser", │
│ "password": "validPass123"}', '发送请求'], │
│ expected_result='接口正常处理请求并返回登录结果', │
│ actual_result='请求成功并返回正确响应'), │
│ TestCaseInfo(case_id='TC_REQ_BODY_002', module='请求参数', │
│ title='验证请求体为空', priority='P1', precondition='接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为空', '发送请求'], │
│ expected_result='接口应拒绝处理或返回明确的错误信息', │
│ actual_result='接口返回状态码400或提示请求体格式错误'), │
│ TestCaseInfo(case_id='TC_REQ_BODY_003', module='请求参数', │
│ title='验证请求体为非JSON格式', priority='P1', │
│ precondition='准备好有效的用户名和密码;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为纯文本(非JSON格式)', │
│ '发送请求'], expected_result='接口应拒绝处理或返回明确的错误信息', │
│ actual_result='接口返回状态码400或提示请求体格式错误'), │
│ TestCaseInfo(case_id='TC_REQ_BODY_004', module='请求参数', │
│ title='验证请求体缺少username字段', priority='P1', │
│ precondition='准备好有效的密码;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"password": "validPass123"}', │
│ '发送请求'], │
│ expected_result='接口应返回明确的错误信息,提示缺少username字段', │
│ actual_result='接口返回状态码400或提示参数缺失'), │
│ TestCaseInfo(case_id='TC_REQ_BODY_005', module='请求参数', │
│ title='验证请求体缺少password字段', priority='P1', │
│ precondition='准备好有效的用户名;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "validUser"}', │
│ '发送请求'], │
│ expected_result='接口应返回明确的错误信息,提示缺少password字段', │
│ actual_result='接口返回状态码400或提示参数缺失'), │
│ TestCaseInfo(case_id='TC_REQ_BODY_006', module='请求参数', │
│ title='验证请求体包含额外字段', priority='P2', │
│ precondition='准备好有效的用户名和密码;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "validUser", │
│ "password": "validPass123", "extraField": "extraValue"}', '发送请求'], │
│ expected_result='接口应正常处理,忽略多余字段', │
│ actual_result='请求成功并返回正确响应(多余字段被忽略)'), │
│ TestCaseInfo(case_id='TC_USERNAME_001', module='请求参数', │
│ title='验证username为空字符串', priority='P1', │
│ precondition='准备好有效的密码;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "", "password": │
│ "validPass123"}', '发送请求'], │
│ expected_result='接口应返回明确的参数校验失败信息', │
│ actual_result='接口返回状态码400或提示参数错误'), │
│ TestCaseInfo(case_id='TC_USERNAME_002', module='请求参数', │
│ title='验证username为1个字符(边界值)', priority='P2', │
│ precondition='数据库中有一个用户名为单字符的有效账号及对应密码;接口服务 │
│ 正常', steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "a", "password": │
│ "对应密码"}', '发送请求'], │
│ expected_result='接口应处理请求并返回登录成功', │
│ actual_result='登录成功,返回code为1'), │
│ TestCaseInfo(case_id='TC_USERNAME_003', module='请求参数', │
│ title='验证username为20个字符(边界值)', priority='P2', │
│ precondition='数据库中有一个用户名为20个字符的有效账号及对应密码;接口服 │
│ 务正常', steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": │
│ "12345678901234567890", "password": "对应密码"}', '发送请求'], │
│ expected_result='接口应处理请求并返回登录成功', │
│ actual_result='登录成功,返回code为1'), │
│ TestCaseInfo(case_id='TC_USERNAME_004', module='请求参数', │
│ title='验证username为21个字符(超出边界)', priority='P2', │
│ precondition='准备好有效的密码;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": │
│ "123456789012345678901", "password": "validPass123"}', '发送请求'], │
│ expected_result='接口应返回明确的参数校验失败信息', │
│ actual_result='接口返回状态码400或提示参数错误'), │
│ TestCaseInfo(case_id='TC_USERNAME_005', module='请求参数', │
│ title='验证username包含特殊字符', priority='P1', │
│ precondition='准备好有效的密码;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "user@name", │
│ "password": "validPass123"}', '发送请求'], │
│ expected_result='接口应返回明确的参数校验失败信息', │
│ actual_result='接口返回状态码400或提示参数错误'), │
│ TestCaseInfo(case_id='TC_USERNAME_006', module='请求参数', │
│ title='验证username包含中文', priority='P1', │
│ precondition='准备好有效的密码;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "用户", │
│ "password": "validPass123"}', '发送请求'], │
│ expected_result='接口应返回明确的参数校验失败信息', │
│ actual_result='接口返回状态码400或提示参数错误'), │
│ TestCaseInfo(case_id='TC_USERNAME_007', module='请求参数', │
│ title='验证username包含空格', priority='P1', │
│ precondition='准备好有效的密码;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "user name", │
│ "password": "validPass123"}', '发送请求'], │
│ expected_result='接口应返回明确的参数校验失败信息', │
│ actual_result='接口返回状态码400或提示参数错误'), │
│ TestCaseInfo(case_id='TC_PASSWORD_001', module='请求参数', │
│ title='验证password为空字符串', priority='P1', │
│ precondition='准备好有效的用户名;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "validUser", │
│ "password": ""}', '发送请求'], │
│ expected_result='接口应返回明确的参数校验失败信息', │
│ actual_result='接口返回状态码400或提示参数错误'), │
│ TestCaseInfo(case_id='TC_PASSWORD_002', module='请求参数', │
│ title='验证password为6个字符(边界值)', priority='P2', │
│ precondition='数据库中有一个账号,其密码为6个字符;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "validUser", │
│ "password": "123456"}', '发送请求'], │
│ expected_result='接口应处理请求并返回登录成功', │
│ actual_result='登录成功,返回code为1'), │
│ TestCaseInfo(case_id='TC_PASSWORD_003', module='请求参数', │
│ title='验证password为20个字符(边界值)', priority='P2', │
│ precondition='数据库中有一个账号,其密码为20个字符;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "validUser", │
│ "password": "12345678901234567890"}', '发送请求'], │
│ expected_result='接口应处理请求并返回登录成功', │
│ actual_result='登录成功,返回code为1'), │
│ TestCaseInfo(case_id='TC_PASSWORD_004', module='请求参数', │
│ title='验证password为5个字符(小于边界)', priority='P2', │
│ precondition='准备好有效的用户名;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "validUser", │
│ "password": "12345"}', '发送请求'], │
│ expected_result='接口应返回明确的参数校验失败信息', │
│ actual_result='接口返回状态码400或提示参数错误'), │
│ TestCaseInfo(case_id='TC_PASSWORD_005', module='请求参数', │
│ title='验证password为21个字符(超出边界)', priority='P2', │
│ precondition='准备好有效的用户名;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "validUser", │
│ "password": "123456789012345678901"}', '发送请求'], │
│ expected_result='接口应返回明确的参数校验失败信息', │
│ actual_result='接口返回状态码400或提示参数错误'), │
│ TestCaseInfo(case_id='TC_PASSWORD_006', module='请求参数', │
│ title='验证password包含允许的特殊字符', priority='P2', │
│ precondition='数据库中有一个密码包含特殊字符的账号;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "validUser", │
│ "password": "Pass@123#"}', '发送请求'], │
│ expected_result='接口应处理请求并返回登录成功', │
│ actual_result='登录成功,返回code为1'), │
│ TestCaseInfo(case_id='TC_METHOD_001', module='请求方法', │
│ title='验证使用GET方法请求', priority='P1', │
│ precondition='准备好有效的用户名和密码;接口服务正常', │
│ steps=['构造GET请求,URL为 /admin/employee/login', '发送请求'], │
│ expected_result='接口应返回方法不允许或资源未找到的错误', │
│ actual_result='接口返回状态码405或404'), │
│ TestCaseInfo(case_id='TC_METHOD_002', module='请求方法', │
│ title='验证使用PUT方法请求', priority='P1', │
│ precondition='准备好有效的用户名和密码;接口服务正常', │
│ steps=['构造PUT请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "validUser", │
│ "password": "validPass123"}', '发送请求'], │
│ expected_result='接口应返回方法不允许或资源未找到的错误', │
│ actual_result='接口返回状态码405或404'), │
│ TestCaseInfo(case_id='TC_METHOD_003', module='请求方法', │
│ title='验证使用DELETE方法请求', priority='P1', │
│ precondition='准备好有效的用户名和密码;接口服务正常', │
│ steps=['构造DELETE请求,URL为 /admin/employee/login', '发送请求'], │
│ expected_result='接口应返回方法不允许或资源未找到的错误', │
│ actual_result='接口返回状态码405或404'), │
│ TestCaseInfo(case_id='TC_SUCCESS_001', module='成功场景', │
│ title='使用正确用户名密码登录', priority='P0', │
│ precondition='拥有一个状态正常且用户名密码正确的管理员账号;接口服务正常' │
│ , steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "admin123", │
│ "password": "correctPassword"}', '发送请求'], │
│ expected_result='接口返回登录成功,且响应结构符合文档', │
│ actual_result='登录成功,返回code为1,data字段包含id, name, token, │
│ userName'), TestCaseInfo(case_id='TC_FAILURE_001', module='失败场景', │
│ title='使用不存在的用户名登录', priority='P1', │
│ precondition='确保使用的用户名不在数据库中;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": │
│ "nonExistingUser", "password": "anyPassword"}', '发送请求'], │
│ expected_result='接口返回登录失败,提示账号不存在', │
│ actual_result='登录失败,返回code为0,data为null,msg为"账号不存在"'), │
│ TestCaseInfo(case_id='TC_FAILURE_002', module='失败场景', │
│ title='使用错误密码登录', priority='P1', │
│ precondition='拥有一个状态正常的管理员账号;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "admin123", │
│ "password": "wrongPassword"}', '发送请求'], │
│ expected_result='接口返回登录失败,提示密码错误', │
│ actual_result='登录失败,返回code为0,data为null,msg为"密码错误"'), │
│ TestCaseInfo(case_id='TC_FAILURE_003', module='失败场景', │
│ title='使用被禁用的账号登录', priority='P1', │
│ precondition='拥有一个状态为"禁用"的管理员账号及密码;接口服务正常', │
│ steps=['构造POST请求,URL为 /admin/employee/login', '设置请求头 │
│ Content-Type: application/json', '请求体为 {"username": "disabledUser", │
│ "password": "correctPassword"}', '发送请求'], │
│ expected_result='接口返回登录失败,提示账号已禁用', │
│ actual_result='登录失败,返回code为0,data为null,msg为"账号已禁用,请联 │
│ 系管理员"'), TestCaseInfo(case_id='TC_FAILURE_004', module='失败场景', │
│ title='使用非法参数登录(用户名含特殊字符且密码过短)', priority='P1', │
│ precondition='接口服务正常', steps=['构造POST请求,URL为 │
│ /admin/employee/login', '设置请求头 Content-Type: application/json', │
│ '请求体为 {"username": "user@", "password": "123"}', '发送请求'], │
│ expected_result='接口返回登录失败,提示参数校验失败', │
│ actual_result='登录失败,返回code为0,data为null,msg包含"参数校验失败"或 │
│ 类似信息'), TestCaseInfo(case_id='TC_SECURITY_001', module='安全相关', │
│ title='验证登录成功后返回的token为JWT格式', priority='P1', │
│ precondition='拥有一个状态正常且用户名密码正确的管理员账号;接口服务正常' │
│ , steps=['使用正确的用户名密码调用登录接口', '检查响应中data.token字段'], │
│ expected_result='登录成功后返回的data.token是有效的JWT格式', │
│ actual_result='登录成功,返回的token符合JWT格式(三段,由点分隔)'), │
│ TestCaseInfo(case_id='TC_SECURITY_002', module='安全相关', │
│ title='验证token可用于后续需要认证的接口', priority='P1', │
│ precondition='已成功登录并获取token;存在一个需要认证的管理端接口(如获取 │
│ 用户信息)', steps=['使用正确的用户名密码调用登录接口,获取token', │
│ '使用获取到的token作为Authorization请求头(如 Bearer │
│ <token>),调用一个需要认证的管理端接口'], │
│ expected_result='token有效,可以用于后续认证', │
│ actual_result='使用返回的token可以成功访问一个需要认证的管理端接口')] │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────── 📋 Task Completion ─────────────────────────────┐
│ │
│ Task Completed │
│ Name: │
│ 根据需求列表,编写测试用例,尽可能覆盖到多种用例设计方法,输出语言为中文。 │
│ 每条测试用例的属性信息如下: │
│ case_id: str = │
│ Field(...,description="测试编号,也就是测试用例ID,例如 TC_LOGIN_001") │
│ module: str = Field(...,description="所属模块,例如 用户登录模块") │
│ title: str = Field(...,description="测试标题,例如 │
│ 正确用户名和密码登录") │
│ priority: str = Field(...,description="测试优先级,例如 │
│ P0、P1、P2、P3") │
│ precondition: str = │
│ Field(...,description="执行测试用例之前需要满足的前提条件") │
│ steps: List[str] = │
│ Field(...,description="测试步骤,可以包含多个操作步骤") │
│ expected_result: str = │
│ Field(...,description="执行测试步骤后应该得到的预期结果") │
│ actual_result: str = Field(..., │
│ description="执行测试步骤后实际得到的预期结果") │
│ Agent: 软件测试用例编写工程师 │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────── 📋 Task Started ──────────────────────────────┐
│ │
│ Task Started │
│ Name: 将测试用例数据插入到数据库 │
│ ID: c464d34c-70e1-48c9-b898-84ce850e7778 │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────── 🤖 Agent Started ──────────────────────────────┐
│ │
│ Agent: 数据库操作工程师 │
│ │
│ Task: 将测试用例数据插入到数据库 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌────────────────────── 🔧 Tool Execution Started (#1) ───────────────────────┐
│ │
│ Tool: db_tool │
│ Args: {'testcaselist': [{'case_id': 'TC_BASIC_001', 'module': │
│ '接口基本信息', 'title': '验证接口名称', 'priority': 'P1', │
│ 'precondition': '拥有接口文档或访问接口元信息的权限', 'steps': │
│ ['访问接口文档或调用获取接口信息的接口'], 'expected_result': │
│ '接口名称显示为... │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Tool db_tool executed with result: 成功插入 38 条测试用例,测试用例ID:['TC_BASIC_001', 'TC_BASIC_002', 'TC_BASIC_003', 'TC_BASIC_004', 'TC_BASIC_005', 'TC_BASIC_006', 'TC_REQ_HEADER_001', 'TC_REQ_HEADER_002', 'TC_REQ_HEADER_003', 'TC_REQ_BODY_001', ...┌─
──────────────────── ✅ Tool Execution Completed (#1) ──────────────────────┐
│ │
│ Tool Completed │
│ Tool: db_tool │
│ Output: 成功插入 38 条测试用例,测试用例ID:['TC_BASIC_001', │
│ 'TC_BASIC_002', 'TC_BASIC_003', 'TC_BASIC_004', 'TC_BASIC_005', │
│ 'TC_BASIC_006', 'TC_REQ_HEADER_001', 'TC_REQ_HEADER_002', │
│ 'TC_REQ_HEADER_003', 'TC_REQ_BODY_001', 'TC_REQ_BODY_002', │
│ 'TC_REQ_BODY_003', 'TC_REQ_BODY_004', 'TC_REQ_BODY_005', │
│ 'TC_REQ_BODY_006', 'TC_USERNAME_001', 'TC_USERNAME_002', │
│ 'TC_USERNAME_003', 'TC_USERNAME_004', 'TC_USERNAME_005', │
│ 'TC_USERNAME_006', 'TC_USERNAME_007', 'TC_PASSWORD_001', │
│ 'TC_PASSWORD_002', 'TC_PASSWORD_003', 'TC_PASSWORD_004', │
│ 'TC_PASSWORD_005', 'TC_PASSWORD_006', 'TC_METHOD_001', 'TC_METHOD_002', │
│ 'TC_METHOD_003', 'TC_SUCCESS_001', 'TC_FAILURE_001', 'TC_FAILURE_002', │
│ 'TC_FAILURE_003', 'TC_FAILURE_004', 'TC_SECURITY_001', 'TC_SECURITY_002'] │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
[Finalize] todos_count=0, todos_with_results=0
┌─────────────────────────── ✅ Agent Final Answer ───────────────────────────┐
│ │
│ Agent: 数据库操作工程师 │
│ │
│ Final Answer: │
│ 测试用例信息已插入数据库 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────── 📋 Task Completion ─────────────────────────────┐
│ │
│ Task Completed │
│ Name: 将测试用例数据插入到数据库 │
│ Agent: 数据库操作工程师 │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────── Crew Completion ──────────────────────────────┐
│ │
│ Crew Execution Completed │测试用例信息已插入数据库
│ Name: crew │
│ ID: 4336200d-76aa-476a-bbce-d608479133a4 │
│ Final Output: 测试用例信息已插入数据库 │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────── Tracing Status ───────────────────────────────┐
│ │
│ Info: Tracing is disabled. │
│ │
│ To enable tracing, do any one of these: │
│ • Set tracing=True in your Crew/Flow code │
│ • Set CREWAI_TRACING_ENABLED=true in your project's .env file │
│ • Run: crewai traces enable │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
测试用例已导出到:D:\Python\Code\fastApiProject1\AIAgent\exports\测试用例_20260911_175414.xlsx
Process finished with exit code 0
数据库中执行:
sql
USE test_platform;
SELECT COUNT(*) AS total
FROM test_case_info;
SELECT case_id, module, title, priority, steps
FROM test_case_info
ORDER BY case_id;
如果数据库客户端已经打开表格,需要点击刷新或重新执行查询。客户端页面中显示的 (N/A) 常常只是空白编辑行,并不代表已经存在一条数据。
目前所生成的测试用例大部分是比较不细致的,我们可以不断地对其进行完善。
14. 常见报错与解决方法
14.1 No module named 'pymysql'
原因:SQLAlchemy URL 使用了 mysql+pymysql,但当前虚拟环境没有安装驱动。
powershell
.\venv\Scripts\python.exe -m pip install pymysql
14.2 OPENAI_API_KEY environment variable is not set
原因通常不是聊天模型,而是 PDFSearchTool 的 Embedding 配置未被当前版本识别,于是工具回退到默认 OpenAI Embedding。
排查:
- 确认使用的是
embedding_model配置; - 确认
provider和内部字段名与当前版本匹配; - 确认
EMBEDDING_API_KEY已配置; - 不要通过打印 Key 来验证,只判断它是否为空。
14.3 Model does not exist
请求已经到达服务商,但 EMBEDDING_MODEL 对应的模型不存在,或把聊天/重排序模型误填成了向量模型。
解决:在服务商控制台确认一个支持 Embedding 接口的准确模型 ID,再更新配置。聊天模型和 Embedding 模型需要分别配置。
14.4 Agent 的 llm 校验失败
典型信息:
text
Input should be a valid string
Input should be a valid dictionary or instance of BaseLLM
原因:将 LangChain ChatOpenAI 对象传给了只接受 CrewAI LLM 的 Agent 版本。使用:
python
from crewai import LLM
并把 LLM(...) 实例传给 Agent。
14.5 'dict' object has no attribute 'model_dump'
原因:工具参数经过序列化后变成了字典。用 TestCaseInfo.model_validate(item) 先还原为 Pydantic 对象,再调用 model_dump()。
14.6 'list' object has no attribute 'test_case_list'
原因:代码以为拿到 TestCaseInfoList,实际上拿到的是普通列表。统一函数契约:
- CrewAI 结构化输出:
TestCaseInfoList; - 数据库函数参数:
list[TestCaseInfo | dict]; - 调用入库时:
insert_test_cases(result.test_case_list)。
14.7 程序显示完成,但数据库没有数据
依次检查:
- 日志中是否真的出现
db_tool成功结果,而不是 Tool Error; - 工具是否返回了大于 0 的插入数量;
- 是否连接到了预期的 host、port 和 database;
- 是否执行并提交了事务;
- 数据库客户端是否刷新;
- 是否发生主键冲突并整体回滚;
- Agent 是否只输出"插入成功",却没有真正调用工具。
14.8 PDF 找不到
不要依赖启动目录下的相对路径:
python
pdf_path = Path(__file__).with_name("api_doc.pdf")
14.9 Excel 无法保存
如果同名文件正在 Excel 中打开,Windows 可能拒绝覆盖。关闭文件,或继续使用带时间戳的新文件名。
15. 从 PDF 扩展到 DOC 和数据库需求
推荐抽象成统一的"需求文本提供器":
python
from typing import Protocol
class RequirementSource(Protocol):
def load(self) -> str:
...
然后为不同来源实现适配器:
text
PdfRequirementSource ─┐
DocxRequirementSource ─┼─> 统一需求文本/检索接口 ─> 需求分析 Agent
DbRequirementSource ─┘
- PDF:继续使用 PDFSearchTool 或先抽取文本再建立向量索引;
- DOCX:读取段落与表格,保留标题层级;
- 数据库:用只读账号按需求 ID 查询,禁止 Agent 自由拼接 SQL;
- 大文档:统一切片、Embedding、检索;
- 小文档:可以直接将完整文本作为 Task 输入。
数据库需求读取工具应采用白名单字段和参数化查询:
python
from sqlalchemy import text
def load_requirement(requirement_id: int) -> str:
statement = text("""
SELECT title, content
FROM requirement
WHERE id = :requirement_id
""")
with engine.connect() as connection:
row = connection.execute(
statement,
{"requirement_id": requirement_id},
).mappings().one()
return f"标题:{row['title']}\n内容:{row['content']}"
16. 测试用例设计方法如何写入 Prompt
为了让结果不只是"多写几条",应在 Task 中明确测试方法:
| 方法 | 适用位置 | 登录示例 |
|---|---|---|
| 等价类 | 输入可分为有效与无效集合 | 合法用户名、非法字符、用户不存在 |
| 边界值 | 字段存在长度或数值范围 | 用户名 1/20 位及 0/21 位,密码 6/20 位及 5/21 位 |
| 判定表 | 多条件组合决定结果 | 用户存在、密码正确、账号启用的组合 |
| 场景法 | 完整业务链路 | 登录成功后携带 Token 访问管理端接口 |
| 状态迁移 | 对象存在状态变化 | 账号从启用变禁用后不能继续登录 |
| 错误推测 | 基于经验补充异常 | 重复提交、超长字符、特殊字符、网络超时 |
| 安全测试 | 鉴权和敏感信息 | 伪造 Token、过期 Token、日志泄露、HTTPS |
建议的任务提示词:
text
请根据上游需求分析结果生成测试用例:
1. 覆盖正常、异常、边界、状态、响应结构和安全场景;
2. 明确使用等价类、边界值、判定表、场景法和错误推测法;
3. 每条用例只验证一个主要目标;
4. case_id 唯一且稳定;
5. 步骤必须可执行,预期结果必须可验证;
6. 文档未明确的规则标记为待确认,不自行编造业务结论;
7. 严格输出 TestCaseInfoList JSON,不附加 Markdown 说明。
17. 当前方案的改进方向
17.1 把"生成"和"落库"适度解耦
更稳健的生产流程通常是:
text
LLM 生成 → Pydantic 校验 → 人工/规则审核 → 确定性代码入库
数据库 Agent 适合演示 Tool 调用和多智能体编排;若追求可控性,可以直接在 Python 主流程中调用 insert_test_cases(),这样入库不再取决于 Agent 是否决定调用工具。
17.2 增加业务校验
Pydantic 当前只校验类型,还可以增加:
priority只能是 P0~P3;case_id必须满足命名规则;steps至少包含一步;actual_result默认"未执行";- 用例 ID 不得重复。
python
from typing import Literal
from pydantic import BaseModel, Field
class StrictTestCaseInfo(BaseModel):
case_id: str = Field(pattern=r"^TC_[A-Z0-9_]+_\d{3}$")
priority: Literal["P0", "P1", "P2", "P3"]
steps: list[str] = Field(min_length=1)
actual_result: str = "未执行"
17.3 可观测性
记录以下非敏感信息:
- 每个 Task 的开始、结束、耗时和状态;
- 文档名称、页数和检索片段数量;
- Pydantic 校验失败字段;
- 生成用例数量;
- 数据库插入数量和失败原因;
- Excel 输出路径。
不要记录 API Key、密码、完整 Token、用户真实敏感数据或完整模型请求头。
17.4 稳定性与维护
- 使用 Alembic 管理数据库表结构升级;
- 为外部模型请求增加超时、限次重试和清晰错误信息;
- 给 Prompt 加版本号,便于比较生成质量;
- 对 RAG 检索结果做来源标记;
- 在正式入库前增加人工审核状态;
- 为
normalize_test_cases、数据库事务和 Excel 导出编写单元测试; - 固定经过验证的依赖版本,升级 CrewAI 后执行回归测试。
18. 最终知识链
这个项目的关键不是"让三个 Agent 聊天",而是建立一条受约束、可验证的数据流水线:
- RAG 负责找到需求依据:PDF 被切片、向量化并按语义检索。
- 需求 Agent 负责理解:把 PRD 转换成业务规则和测试点。
- 用例 Agent 负责设计:运用测试方法生成覆盖充分的用例。
- Pydantic 负责守门:统一字段、层级和数据类型。
- SQLAlchemy Tool 负责执行:使用事务把每条用例真实写入 MySQL。
- OpenPyXL 负责交付:把同一份结构化数据导出为 Excel。
- 日志与查询负责验收:不能以 Agent 的口头回复代替真实执行结果。
当输入来源换成 DOCX 或数据库时,只需要替换"需求获取"部分;后续的需求分析、用例生成、结构校验、入库和导出仍可以复用,这就是该架构最有价值的地方。
本次内容就分享到这里,感谢大家观看,感兴趣的话可以看我后面的更新内容。