# 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.py(keyring 降级)— 4 用例 - ✅ ddic.py(XML/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.py(30 个 API 方法)— 0 用例 - ❌ commands/*.py(11 个命令模块)— 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 # 共享 fixture(SAPConfig 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`。