关于 generate-abap-unit-test
generate-abap-unit-test 是面向 ABAP 开发者的类级单元测试生成技能,可分析依赖、设计测试用例并生成 ABAP Unit 测试代码。它使用基于接口的测试替身,并支持预览、写入、激活和运行测试的流程。
主要功能
- 01读取类源码、方法和依赖
- 02按分支、错误路径和结果设计用例
- 03使用接口实现测试替身
- 04生成 ABAP Unit 测试类
- 05支持代码写入、激活与测试运行
适用场景
- 01为 ABAP 类的公开方法生成测试
- 02覆盖方法中的分支和异常路径
- 03隔离注入依赖以测试类逻辑
- 04运行测试并定位失败原因
MCP 相关
技能通过 ARC-1 读取和更新 SAP 类、激活代码并运行单元测试;同时使用 mcp-sap-docs 查询 ABAP Unit 和测试替身模式。未说明向 MCP 客户端提供服务。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ arc-mcp/ arc-1 --skill generate-abap-unit-test安装后在 Claude Code 中输入/generate-abap-unit-test确认已加载。
详细介绍
生成 ABAP 单元测试
通过依赖分析、测试替身和方法级修改,为类生成 ABAP 单元测试。
此技能结合 ARC-1(SAP 系统访问)和 mcp-sap-docs(文档与最佳实践),复现 SAP Joule 的“单元测试生成”能力,适用于 ABAP 类。与面向 CDS 实体的 CDS 单元测试技能不同,此技能面向 ABAP 类,并使用基于接口的测试替身,而非 CDS 测试替身框架。
智能默认值(静默应用,不要询问)
| 设置 | 默认值 | 原因 |
|---|---|---|
| 测试类名称 | ltc_<short_class_name> | 全局类的 ABAP Unit 测试属于其本地 testclasses(CCAU)包含程序 |
| 测试方法 | 所有公共方法 | 用户看到列表后可以缩小范围 |
| 包 | $TMP | 快速原型开发 |
| 风险级别 | HARMLESS | 测试替身不会修改真实数据 |
| 持续时间 | SHORT | 单元测试应快速运行 |
| 模拟策略 | 基于接口的测试替身 | 依赖注入中最简洁的模式 |
输入
用户提供要测试的 ABAP 类(例如,ZCL_TRAVEL_HANDLER)。
只需提供类名称。如果用户只提供类名称,则应用智能默认值并立即继续。
用户也可以指定:
- 测试类名称(默认:目标类的
testclasses包含程序中的本地类ltc_<short_class_name>) - 测试方法(默认:所有公共方法)
- 包(默认:
$TMP) - 传输请求(仅非
$TMP包需要)
步骤 1:收集类上下文
读取类、其方法、依赖项和任何现有测试。
1a. 读取完整类源代码
优先使用结构化格式,以便通过一次调用获取元数据和所有包含程序(包括现有测试):
SAPRead(type="CLAS", name="<class_name>", format="structured")
此操作会返回包含元数据(描述、类别、包)和分解后源代码(main、testclasses、definitions、implementations、macros)的 JSON。testclasses 字段包含现有测试代码(如果有),无需单独获取即可用于分析已有覆盖范围。
或者,仅获取主源代码:
SAPRead(type="CLAS", name="<class_name>")
1b. 列出所有带签名的方法
SAPRead(type="CLAS", name="<class_name>", method="*")
返回所有方法及其签名和可见性(公共/受保护/私有)。使用此信息了解类 API,并确定要测试的方法。
1c. 获取依赖项上下文
SAPContext(type="CLAS", name="<class_name>")
自动提取所有依赖项,并获取每项依赖的压缩公共 API 契约。由此可以了解:
- 实现的接口及其方法
- 超类层次结构
- 注入的依赖项(构造函数参数)
- 使用的类及其公共 API
- 访问的数据库表
对于依赖链较深的复杂类,使用 depth=2:
SAPContext(type="CLAS", name="<class_name>", depth=2)
1d.(可选)读取现有测试类
如果在步骤 1a 中使用了 format="structured",testclasses 字段已包含现有测试代码(如果没有则为 null)——跳过此步骤。
否则,单独获取测试类:
SAPRead(type="CLAS", name="<class_name>", include="testclasses")
如果不存在测试类,此操作可能失败——这没关系。如果存在测试,则分析它们,以避免重复覆盖,并遵循现有测试模式。
步骤 2:分析方法并提出测试用例
针对每个公共方法,分析源代码以确定可测试的场景。
依赖项分类
对类使用的每个依赖项进行分类:
| 类别 | 查找内容 | 测试策略 |
|---|---|---|
| 可模拟 | 通过接口(if_*、zif_*)进行的构造函数注入 | 创建实现该接口的测试替身 |
| 可设桩 | 数据库访问(SELECT、INSERT、UPDATE、DELETE) | 在 SETUP 中准备夹具数据,或使用数据库层的测试替身 |
| 透明 | 内部私有/受保护的辅助方法 | 不要模拟——它们会正常执行 |
| 框架 | 对 SAP 框架类的静态调用(例如,cl_abap_context_info) | 封装到可注入接口中,或接受这一限制 |
按方法分析
对于每个公共方法,请确定:
- 分支:IF/CASE/COND/SWITCH 语句——每个分支都需要一个测试
- 错误路径:RAISE EXCEPTION、TRY-CATCH 块——测试错误场景
- 返回值:方法产生的结果——验证输出
- 状态变更:对实例属性或数据库的副作用——验证变更
- 使用的依赖项:调用了哪些可模拟的依赖项——设置预期
向用户展示输出
按方法分组,将已确定的测试用例以编号列表的形式展示:
Identified test cases for ZCL_TRAVEL_HANDLER:
Method: CREATE_TRAVEL (public)
1. [HAPPY] test_create_travel_success — Create travel with valid data, verify travel ID returned
2. [ERROR] test_create_no_customer — Missing customer, expect CX_TRAVEL_ERROR
3. [BRANCH] test_create_with_discount — Discount > 0 triggers calculation path
Method: VALIDATE_DATES (public)
4. [HAPPY] test_dates_valid — End date after begin date
5. [ERROR] test_dates_reversed — Begin date after end date, expect exception
6. [EDGE] test_dates_same_day — Same begin and end date, should be valid
询问用户:“要生成哪些测试用例?(全部 / 指定编号 / 跳过哪些?)”
如果用户回答“全部”,或未提供偏好,则生成所有测试用例。
步骤 3:获取测试框架参考资料
使用 mcp-sap-docs 获取最新的 ABAP Unit 和测试替身模式。
search("ABAP Unit cl_abap_unit_assert")
search("test double interface mock ABAP")
根据返回的文档完善生成的代码模式。需要核实的关键事项:
cl_abap_unit_assert断言方法名称是否正确(assert_equals、assert_initial、assert_not_initial、assert_bound、fail)- 接口的测试替身创建模式
FOR TESTING类和方法语法- 风险级别和持续时间注解
步骤 4:生成测试类
按照以下结构生成完整的 ABAP 测试类:
模板
"! @testing <CLASS_UNDER_TEST>
CLASS <test_class_name> DEFINITION
FINAL
FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PUBLIC SECTION.
PROTECTED SECTION.
PRIVATE SECTION.
CLASS-DATA cut TYPE REF TO <class_under_test>.
" Test doubles for mockable dependencies
CLASS-DATA mock_<dep1> TYPE REF TO <interface_1>.
CLASS-DATA mock_<dep2> TYPE REF TO <interface_2>.
CLASS-METHODS class_setup RAISING cx_static_check.
CLASS-METHODS class_teardown.
METHODS setup.
"! <description of test case 1>
METHODS <test_method_1> FOR TESTING RAISING cx_static_check.
"! <description of test case 2>
METHODS <test_method_2> FOR TESTING RAISING cx_static_check.
" ... one method per selected test case
ENDCLASS.
CLASS <test_class_name> IMPLEMENTATION.
METHOD class_setup.
" One-time initialization — create test doubles
" If using a test double framework:
" mock_<dep1> = cl_abap_testdouble=>create( '<interface_1>' ).
" Or create a local test double class (see below)
ENDMETHOD.
METHOD class_teardown.
" Clean up
ENDMETHOD.
METHOD setup.
" Per-test CUT instantiation with fresh state
" Inject test doubles via constructor
cut = NEW <class_under_test>(
io_<dep1> = mock_<dep1>
io_<dep2> = mock_<dep2>
).
ENDMETHOD.
METHOD <test_method_1>.
" Arrange — configure mock behavior
" cl_abap_testdouble=>configure_call( mock_<dep1>
" )->returning( VALUE #( <expected_return> ) )->and_expect( )->is_called_once( ).
" mock_<dep1>-><method>( ).
" Act — call the method under test
DATA(lv_result) = cut-><method>( <params> ).
" Assert — verify expected behavior
cl_abap_unit_assert=>assert_equals(
act = lv_result
exp = <expected_value>
msg = '<assertion message>' ).
ENDMETHOD.
METHOD <test_method_error>.
" Arrange — set up conditions that trigger the error
" Act & Assert — expect exception
TRY.
cut-><method>( <invalid_params> ).
cl_abap_unit_assert=>fail( msg = 'Expected exception was not raised' ).
CATCH <exception_class> INTO DATA(lx_error).
" Optionally verify exception details
cl_abap_unit_assert=>assert_not_initial(
act = lx_error->get_text( )
msg = 'Exception message should not be empty' ).
ENDTRY.
ENDMETHOD.
" ... more test methods
ENDCLASS.
本地测试替身模式
如果 cl_abap_testdouble 不可用,或者接口较复杂,请创建一个本地测试替身类:
" Local test double — defined in the test include
CLASS lcl_mock_<dep> DEFINITION FOR TESTING.
PUBLIC SECTION.
INTERFACES <interface>.
DATA last_<method>_input TYPE <param_type>.
DATA <method>_result TYPE <return_type>.
ENDCLASS.
CLASS lcl_mock_<dep> IMPLEMENTATION.
METHOD <interface>~<method>.
last_<method>_input = <param>.
<return_param> = <method>_result.
ENDMETHOD.
ENDCLASS.
测试数据生成规则
生成测试数据时,请遵循以下原则:
- 最少数据:使用能够覆盖场景所需的最少值。通常每个测试使用 1-3 条记录。
- 直观的值:选择易于直接验证预期结果的值:
- 对于计算:使用 100.00 - 10.00 = 90.00,而不是 37.83 - 4.27
- 对于状态检查:使用 IF/CASE 表达式中的确切状态值
- 对于字符串操作:使用简短且易识别的字符串
- 类型正确:严格匹配 ABAP 类型:
- NUMC 字段使用
'001'(带前导零的字符串) - DEC/CURR 字段使用
'100.00' - DATS 字段使用
'20260101' - ABAP_BOOL 使用
abap_true/abap_false
- NUMC 字段使用
- 确定性:绝不使用
sy-datum、sy-uzeit、cl_abap_context_info=>get_system_date( )或随机值 - 相互隔离:每个测试方法都应自成一体——setup 方法确保 CUT 状态是全新的
命名约定
- 测试类:在类的
testclassesinclude 中使用本地类ltc_<short_class_name> - 测试方法:
test_<method>_<scenario>(例如:test_create_success、test_validate_no_date) - 方法名使用 snake_case(ABAP 测试方法的惯例)
- 方法名不超过 30 个字符
- Mock 变量:
mock_<short_dep_name>(例如:mock_persistence、mock_validator)
步骤 5:预览并确认
向用户展示完整生成的 testclasses include 源代码,并询问:
“这是生成的 testclasses include。要将其写入 SAP 系统吗?(是 / 先编辑 / 取消)”
如果用户想要编辑,请在继续之前进行修改。
步骤 6:写入、激活并测试
6-pre. 对生成的代码进行 lint 检查(可选)
写入前,根据 lint 规则验证生成的代码:
SAPLint(action="lint", source="<generated_testclasses_include>", name="<class_under_test>")
继续之前请修复所有 lint 检查问题。启用时(默认:开启),写入前也会自动运行 lint 验证。
6a. 将本地测试类写入目标类的 testclasses include
SAPWrite(action="update", type="CLAS", name="<class_under_test>", include="testclasses", source="<generated_testclasses_include>", transport="<transport>")
这是写入本地 include,而不是创建单独的全局类。不要传递锁句柄,也不要在 ABAP 源代码中添加锁定/解锁代码;ARC-1 会在内部执行 ADT 锁定 -> 初始化缺失的 CCAU include -> 写入 -> 解锁流程。对于新建的类,ARC-1 会在写入前自动创建缺失的 testclasses include。
6b. 更新现有测试
SAPWrite(action="update", type="CLAS", name="<class_under_test>", include="testclasses", source="<full_updated_testclasses_include>", transport="<transport>")
对于现有本地测试方法的小幅修复,请在包含文件存在后使用方法手术:
SAPWrite(action="edit_method", type="CLAS", name="<class_under_test>", method="<ltc_class>~<failing_method>", source="<fixed_method_body>")
注意: 对于更新操作,建议使用 transport,但并非总是必需。如果未显式提供 transport,ARC-1 会自动传播锁提供的 corrNr。
6c. 激活被测类
SAPActivate(type="CLAS", name="<class_under_test>")
激活会返回结构化响应,其中包含详细的错误和警告消息以及行号。利用这些信息精确定位问题。
6d. 运行单元测试
SAPDiagnose(action="unittest", type="CLAS", name="<class_under_test>")
6e. 报告结果并修复失败项
向用户展示:
- 通过 / 失败 / 跳过的测试数量
- 对于任何失败项:断言消息以及失败的测试方法
- 针对任何失败项的建议修复方案
如果测试失败:
- 分析失败消息
- 判断问题是由 mock 设置、断言还是 CUT 逻辑引起
- 使用方法手术修复具体测试方法:
SAPWrite(action="edit_method", type="CLAS", name="<class_under_test>", method="<ltc_class>~<failing_method>", source="<fixed_source>") - 重新激活并运行
错误处理
常见问题和修复方法
| 错误 | 原因 | 修复方法 |
|---|---|---|
class_setup 失败 | CUT 实例化错误 — 缺少构造函数参数或类型错误 | 检查 CUT 构造函数签名,确保注入了所有必需的依赖项 |
| Mock 注入失败 | 构造函数参数类型不匹配 — mock 未实现预期接口 | 验证接口名称,检查 CUT 是否要求具体类而非接口 |
| 断言失败 | 预期值或测试数据错误 | 重新检查 CUT 方法逻辑,调整预期值或输入数据 |
| 激活错误 | 生成的代码存在语法错误 — 拼写错误、类型错误或缺少变量声明 | 阅读激活错误消息,修复语法,然后重新激活 |
找不到 cl_abap_testdouble | 系统不支持测试替身框架(较旧版本) | 改用本地测试替身类模式 |
| 在 CUT 中找不到方法 | 方法为私有或受保护 — 测试无法调用 | 仅测试公共方法;对于受保护的方法,使用 FRIENDS 子句 |
缺少 testclasses/CCAU 包含文件 | 新建的类尚无 ABAP Unit 包含文件 | 使用 SAPWrite(action="update", type="CLAS", include="testclasses", source=...);ARC-1 会自动初始化 |
在 edit_method 中找不到方法 | 本地 ltc_* 方法尚不存在,或使用了错误的本地类名称 | 首先使用 update include="testclasses" 写入完整的 testclasses 包含文件,然后对现有方法使用 edit_method |
备注
BTP 与 On-Premise 的差异
- BTP:将本地
ltc_*测试类写入自定义 Z*/Y* 类。必须使用 ABAP Cloud 语法 — 不得使用经典 ABAP 语句。仅可使用已发布的 API 进行 mock。cl_abap_testdouble可用。构造函数注入是标准模式。 - On-Premise:完全灵活。可以使用经典 ABAP。7.51 及更高版本提供
cl_abap_testdouble。在较旧的系统上,使用本地测试替身类。
此技能不会执行的操作
- 不进行 CDS 单元测试:对于 CDS 实体,请使用 generate-cds-unit-test 技能(使用 CDS 测试替身框架)
- 不进行集成测试:此功能生成的是带有 mock 依赖项的隔离单元测试,而不是端到端流程
- 不进行性能测试:使用 ABAP 分析器(SAPDiagnose)进行性能分析
- 不进行 RAP 行为测试:RAP BO 测试替身需要使用单独的模式
何时使用此技能
- 类的公共方法中包含复杂业务逻辑时
- 类使用依赖注入(带接口的构造函数参数)时 — 这是 mock 的理想场景
- 用户希望从测试覆盖率入手进行 TDD 时
- 重构类之前,希望先编写回归测试时
- 不适用于没有公共方法的类或纯数据容器(结构体 / 常量)——如果第 2 步的分析发现没有可测试场景(没有分支、错误路径或有意义的返回值),请提醒用户:“此类没有可供测试的非平凡公共方法。请考虑单元测试是否能为此带来价值。”