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

15 KiB
Raw Blame History

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

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

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