Files
sap-cli-skill/docs/dev/test-plan.md
T
吴让宇 c5905a5b1e refactor: 本仓升为唯一源(原 sap-cli 源码仓归档)
方向反转:此前 SKILL.md 是「模板渲染产物」、sap-cli 是源;现 sap-cli 归档,
sap-cli-skill 承接开发与分发,SKILL.md 回归手工维护的正本。

迁移(来自 sap-cli,共 104 文件):
- tests/           692 例测试(15 个文件的内联 sys.path 改指 assets/)
- openspec/        SDD 规格与归档变更(42 文件)
- docs/            开发文档与 ADT 原理(含 dev/CLAUDE.md、AGENTS.md)
- .claude/         rules 副本 + settings.json(供 Claude Code)
- .github/ .hermes/ .pre-commit-config.yaml .editorconfig CLAUDE.md
- scripts/ 保持仅 setup.py(pack_skill.py 已随旧仓归档,不迁)

修复(迁移暴露的真实缺陷):
- assets/pyproject.toml 的 build-backend 写作 `setuptools.backends._legacy:_Backend`,
  该模块在 setuptools 中不存在 → `pip install -e` 从来装不上。改为 build_meta。
  实测:临时 venv 安装成功,sap-cli --help 正常列出 31 个命令
- pyproject readme 指向不存在的 assets/README.md(editable 安装会失败)→ 改内联文本
- pyproject urls 改指 sap-cli-skill

机制调整:
- .github/workflows/ci.yml 适配 assets/ 布局;顶部注明该工作流仅 GitHub 执行,
  本仓在 Gitee 不会自动跑
- pre-commit 增本地测试门禁(Gitee 上真正生效的那道)
- .gitignore 合并旧仓完整规则(保留 log/ 下 md 知识库入库,只忽略运行日志)
- 大文件上限 100KB→1MB(架构图 512KB)

守卫测试 tests/unit/test_repo_guards.py(10 → 18 例):
- SKILL.md 须记录 parser 全部 CLI 命令 / 铁律 1-5 须为真实小节标题 / 示例不得违反铁律 5
- references/ 规则齐备;.claude/rules 与 references 必须一致(实测抓到一次真实漂移)
- VERSION == sapcli.__version__ == README 版本
- 仓内不得再出现 pack_skill.py / skill-src(防废弃流程回潮)

698 tests OK;editable 安装与 CLI 入口经临时 venv 实测通过。
docs/RELEASING.md 重写为单源开发流程。
2026-09-11 00:40:15 +08:00

345 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# sap-cli 测试方案
> 版本:2.1 | 20 命令 · 16 类型 · 30 API 方法
> 日期:2026-06-09
---
## 一、现状分析
### 1.1 代码架构(四层)
```
┌─────────────────────────────────────────────────┐
│ CLI 层 parser.py + app.py │ ← 参数解析、命令路由
├─────────────────────────────────────────────────┤
│ 命令层 commands/*.py (11 模块, 20 命令) │ ← 业务编排
├─────────────────────────────────────────────────┤
│ API 层 client.py (30 方法) │ ← ADT REST 通信
├─────────────────────────────────────────────────┤
│ 基础层 types/config/auth/ddic/manifest/... │ ← 纯逻辑、无网络
└─────────────────────────────────────────────────┘
```
### 1.2 现有测试
| 文件 | 行数 | 状态 | 说明 |
|------|------|------|------|
| `tests/test_sapcli.py` | 695 | ✅ 活跃 | 68 用例(63 pass + 5 skip |
| `tests/test_batch.py` | 840 | ⚠️ 过时 | 旧版批量测试,API 签名已变 |
| `tests/test_main.py` | 833 | ⚠️ 过时 | 旧版 E2E,引用旧入口 |
**test_sapcli.py 覆盖的模块:**
- ✅ types.py(注册、解析)— 6 用例
- ✅ config.py(环境变量)— 2 用例
- ✅ auth.pykeyring 降级)— 4 用例
- ✅ ddic.pyXML/DDL 构建)— 7 用例
- ✅ manifest.py(增删查)— 5 用例
- ✅ scanner.py(目录扫描)— 2 用例
- ✅ sorter.py(拓扑排序)— 4 用例
- ✅ exceptions.py — 2 用例
- ✅ scaffold.py(模板生成)— 4 用例
- ✅ xml_utils.py — 4 用例
- ✅ XML 注入防御 — 3 用例
- ✅ 模块结构检查 — 5 用例
- ✅ OpenSpec 文件检查 — 3 用例
- ✅ output.py — 3 用例
- ✅ analyze.py — 4 用例
**未覆盖的模块(缺口):**
- ❌ client.py30 个 API 方法)— 0 用例
- ❌ commands/*.py11 个命令模块)— 0 用例
- ❌ cli/parser.py(参数校验)— 0 用例
- ❌ cli/app.py(路由逻辑)— 0 用例
---
## 二、测试分层设计
### 分层策略
```
┌─────────────┐
│ E2E 测试 │ ← 真实 SAP 系统
│ (手动) │ 仅在上线前执行
├─────────────┤
│ 集成测试 │ ← Mock HTTP Server
│ (自动化) │ 验证 client↔ADT 通信
├─────────────┤
│ 单元测试 │ ← unittest.mock
│ (自动化) │ 验证各模块独立逻辑
├─────────────┤
│ 静态分析 │ ← pylint / mypy
│ (CI 自动) │ 语法、类型、规范
└─────────────┘
```
| 层级 | 目标 | 技术 | 运行频率 | 依赖 |
|------|------|------|---------|------|
| 静态分析 | 语法/类型/规范 | pylint, mypy | 每次 commit | 无 |
| 单元测试 | 模块独立逻辑 | unittest + mock | 每次 commit | 无 |
| 集成测试 | API 通信正确性 | unittest + responses/mock HTTP | 每次 commit | 无 |
| E2E 测试 | 端到端真实流程 | 手动执行 | 发布前 | SAP 系统 |
---
## 三、单元测试设计(优先级 P0)
> 目标:每个模块的纯逻辑都有测试,不依赖网络。
### 3.1 保留现有 68 用例
全部保留,但修复 5 个 skip
- `test_multi_profile` / `test_profile_fallback`:现在 config.py 已支持 `--profile`,应改为 pass
- `test_list_objects` / `test_pretty_print` / `test_transport_info`:方法已加回 client.py,应取消 skip
### 3.2 新增用例清单
#### A. client.py — API 层(30 方法)
每个方法测 3 个场景:成功响应、HTTP 错误、XML 解析。
| 方法 | 正常用例 | 错误用例 | mock 要点 |
|------|---------|---------|-----------|
| `login()` | 返回有效 session | 401 → LoginError | mock session.get |
| `lock()` | 返回 lock_handle + corr_nr | 403 → ObjectLockedError | mock session.post |
| `unlock()` | 返回 True | 非 200 返回 False | mock session.post |
| `set_source()` | 返回 True | 非 200 返回 False | mock session.put |
| `get_source()` | 返回源码字符串 | 404 → ObjectNotFoundError | mock session.get |
| `delete_object()` | 返回 (True, msg) | 非 200 (False, msg) | mock session.delete |
| `create_object()` | 返回 (obj_uri, src_uri) | 409 → ObjectAlreadyExistsError | mock session.post |
| `activate()` | 返回 (True, []) | 返回 (False, [errors]) | mock session.post |
| `syntax_check()` | 返回 (True, []) | 返回 (False, [errors]) | mock session.post |
| `get_object_status()` | 返回 status dict | 404 → ObjectNotFoundError | mock session.get |
| `object_exists()` | 返回 True/False | — | mock session.get |
| `list_objects()` | 返回 list[dict] | 空 → [] | mock session.get |
| `where_used()` | 返回 list[dict] | 无引用 → [] | mock session.post |
| `search_code()` | 返回 list[dict] | 无结果 → [] | mock session.post |
| `read_source_for_diff()` | 委托 get_source | — | mock self.get_source |
| `get_cds_source()` | 返回 DDL 字符串 | 404 → ObjectNotFoundError | mock session.get |
| `create_cds()` | 创建+写入+返回 URI | 失败 → SapCliError | mock lock/set_source/unlock |
| `create_package()` | 返回 True | 非 200 → False | mock session.post |
| `get_package_info()` | 返回 dict | 404 → SapCliError | mock session.get |
| `transport_info()` | 返回 dict | 404 → SapCliError | mock session.get |
| `transport_release()` | 返回 True | 非 200 → False | mock session.post |
| `transport_objects()` | 返回 list[dict] | 空 → [] | mock session.get |
| `atc_check()` | 返回 (bool, list) | 无发现 → (True, []) | mock session.post |
| `pretty_print()` | 返回格式化源码 | 失败 → SapCliError | mock session.post |
| `list_transport_requests()` | 返回 list[dict] | 空 → [] | mock session.get |
| `create_transport_request()` | 返回 corr_nr | 失败 → SapCliError | mock session.post |
| `get_transport_request()` | 返回 corr_nr 或 None | 空 → None | mock session.get |
| `create_function_group()` | 成功 | 失败 → SapCliError | mock session.post |
| `function_group_exists()` | True/False | — | mock session.get |
| `create_ddic_object()` | 成功写入 | 失败 → SapCliError | mock self._put_ddic_xml |
**小计:~90 用例**
#### B. commands/*.py — 命令层(11 模块)
每个命令测:正常流程 + 错误处理 + 参数缺失。
| 命令 | 模块 | 正常用例 | 错误用例 |
|------|------|---------|---------|
| `create` | crud.py | 创建成功(4种模板+--source+--definition | 对象已存在、类型不支持 |
| `download` | crud.py | 下载成功、文件名正确 | 对象不存在、无源码 URI |
| `sync` | crud.py | 五步同步成功 | 语法检查失败、激活失败 |
| `info` | crud.py | 显示元数据 | 对象不存在 |
| `delete` | crud.py | 确认删除、取消删除 | 对象不存在 |
| `init` | batch.py | 扫描→生成 manifest | 空目录、无 SAP 连接 |
| `refresh` | batch.py | 更新状态 | — |
| `sync --all` | batch.py | 批量同步+拓扑排序 | 循环依赖、fail-fast |
| `list` | search.py | 列出对象 | 空 |
| `whereused` | search.py | 找到引用 | 无引用 |
| `search` | search.py | 搜索结果 | 无结果 |
| `diff` | diff_cmd.py | 有差异/无差异 | — |
| `cds` | cds.py | 下载DDL/创建CDS | — |
| `package` | package_cmd.py | 创建/查询包 | — |
| `transport` | transport.py | 列出/详情/释放 | — |
| `check` | quality.py | ATC检查有/无发现 | — |
| `format` | quality.py | 格式化成功 | — |
| `analyze` | analyze.py | 分析依赖 | 无依赖 |
| `scaffold` | scaffold.py | 4种模板+无模板列出 | — |
| `config` | config_cmd.py | show/list-profiles/set | — |
| `auth` | auth.py | login/logout/status | keyring 不可用 |
**小计:~60 用例**
#### C. cli/ — 入口层
| 测试场景 | 说明 |
|---------|------|
| 无参数 → exit(1) + 帮助 | `app.py` 检测 no command |
| --profile DEV | 读 [DEV] section |
| --config 自定义路径 | 读指定文件 |
| config set host 1.2.3.4 | 写入 config.ini |
| config set invalid_key | 报错 |
| auth status | keyring 状态 |
| scaffold 无 --template | 列出模板 |
| scaffold --template xxx --name ZZZ | 生成模板 |
**小计:~10 用例**
### 3.3 单元测试总计
| 类别 | 现有 | 新增 | 合计 |
|------|------|------|------|
| 基础层 | 68 | 0 | 68 |
| API 层 (client.py) | 0 | ~90 | 90 |
| 命令层 (commands/) | 0 | ~60 | 60 |
| 入口层 (cli/) | 0 | ~10 | 10 |
| **总计** | **68** | **~160** | **~228** |
---
## 四、集成测试设计(优先级 P1)
> 目标:验证 client.py 与真实 ADT API 的通信协议是否正确。
### 4.1 技术方案:`responses` 库 mock HTTP
```python
import responses
@responses.activate
def test_login_success():
responses.add(responses.GET, "https://sap.example.com/sap/bc/adt/...",
status=200, headers={"x-csrf-token": "TOKEN123"})
client = ADTClient("sap.example.com", "100", "USER", "PASS")
client.login()
assert client.csrf_token == "TOKEN123"
```
### 4.2 集成测试用例
按 ADT API 端点分组,验证 HTTP 方法和 XML 请求体:
| 端点组 | 测试场景 | 数量 |
|--------|---------|------|
| 认证 | login → CSRF token 获取 | 2 |
| 锁管理 | lock/unlock → 正确的 If-Match header | 4 |
| 源码读写 | get/set_source → 正确的 Content-Type | 4 |
| 对象 CRUD | create/delete → 正确的 XML body | 6 |
| DDIC | create_ddic → 正确的 XML namespace | 4 |
| 激活/检查 | activate/syntax_check → 正确的 XML 响应解析 | 4 |
| 搜索 | list/search/whereused → URL 参数编码 | 4 |
| 传输 | transport list/info/release → URL 拼接 | 4 |
| 质量 | atc_check/pretty_print → 请求体格式 | 4 |
| **合计** | | **~36** |
---
## 五、E2E 测试设计(优先级 P2)
> 目标:端到端验证完整用户场景。
### 5.1 前置条件
- 需要一台可连接的 SAP 系统
- 使用测试用户和 `$TMP`
- 所有操作可逆(删除测试对象)
### 5.2 测试场景
| # | 场景 | 步骤 | 验证点 |
|---|------|------|--------|
| E1 | 完整 CRUD | create → info → download → sync → delete | 每步输出正确,对象最终不存在 |
| E2 | DDIC 对象 | create domain → create dataelement → create table | 依赖顺序正确 |
| E3 | 批量同步 | init → 修改本地文件 → sync --all | 所有对象同步成功 |
| E4 | 多 Profile | config set → --profile DEV → download | 读取正确配置 |
| E5 | 传输管理 | create transport → create object → release | 传输号关联正确 |
**E2E 测试建议用手动执行**,因为:
- 依赖真实 SAP 系统可用性
- 测试数据需要隔离
- 某些操作不可逆(如 transport release
### 5.3 E2E 自动化(可选)
如果 SAP 测试系统长期可用,可用 `tests/e2e_project/` 现有的测试数据做半自动 E2E
```bash
python main.py init --path tests/e2e_project
python main.py sync --all --path tests/e2e_project
```
---
## 六、现有测试处理
| 文件 | 处理 | 原因 |
|------|------|------|
| `tests/test_sapcli.py` | ✅ 保留并扩展 | 当前唯一活跃测试 |
| `tests/test_batch.py` | ❌ 废弃 | 840 行,API 签名已变,与新架构不兼容 |
| `tests/test_main.py` | ❌ 废弃 | 833 行,引用旧入口 `src/` 路径 |
| `tests/test_e2e.md` | ✅ 保留 | E2E 手动测试说明仍有参考价值 |
---
## 七、测试文件组织
```
tests/
├── conftest.py # 共享 fixtureSAPConfig mock、临时目录)
├── test_sapcli.py # 基础层测试(保留现有 68 → 修复 5 skip)
├── unit/ # 新增:单元测试
│ ├── test_client.py # client.py 30 方法
│ ├── test_commands.py # commands/ 11 模块
│ └── test_cli.py # parser + app 路由
├── integration/ # 新增:集成测试
│ ├── test_adt_auth.py # 认证通信
│ ├── test_adt_crud.py # CRUD 通信
│ ├── test_adt_search.py # 搜索通信
│ ├── test_adt_transport.py # 传输通信
│ └── test_adt_quality.py # 质量通信
├── e2e/ # E2E 手动测试
│ └── test_e2e.md # 手动测试说明
├── e2e_project/ # E2E 测试数据
│ ├── manifest.json
│ ├── reports/
│ ├── classes/
│ └── functions/
└── fixtures/ # 测试固件
├── adt_responses/ # ADT XML 响应样本
│ ├── login_success.xml
│ ├── object_info.xml
│ └── syntax_errors.xml
└── sample_source/ # 示例 ABAP 源码
├── zhello.abap
└── zcl_class.abap
```
---
## 八、实施优先级
| 阶段 | 内容 | 用例数 | 预计工时 |
|------|------|--------|---------|
| **P0** | 修复 5 个 skip + 清理旧测试 | +5 pass, -2 文件 | 1h |
| **P0** | client.py 单元测试 | ~90 | 4h |
| **P1** | commands/ 单元测试 | ~60 | 3h |
| **P1** | cli/ 入口测试 | ~10 | 1h |
| **P2** | 集成测试(mock HTTP | ~36 | 3h |
| **P3** | E2E 手动测试 | 5 场景 | 2h |
| | **合计** | **~228** | **~14h** |
### 立即可做(本次)
1. 修复 5 个 skip 用例 → 68 pass
2. 清理旧测试文件 → 减少 1673 行过时代码
3. 新建测试目录结构
4. 从 client.py 核心方法开始写单元测试
---
## 九、关键技术选型
| 工具 | 用途 | 是否需要安装 |
|------|------|-------------|
| `unittest` | 测试框架 | ✅ 内置 |
| `unittest.mock` | mock client/requests | ✅ 内置 |
| `responses` | mock HTTP 响应(集成测试) | 需要 `pip install responses` |
| `tempfile` | 临时文件/目录 | ✅ 内置 |
| `pytest` | 可选:更简洁的断言 + fixture | 需要 `pip install pytest` |
**建议**:保持 `unittest` 框架(不引入 pytest),减少外部依赖。集成测试用 `unittest.mock.patch` 替代 `responses`