mcpField
G
generate-abap-unit-test

generate-abap-unit-test

github.com/arc-mcp测试与质量239264 次安装2 天前更新
代码仓库

关于 generate-abap-unit-test

generate-abap-unit-test 是面向 ABAP 开发者的类级单元测试生成技能,可分析依赖、设计测试用例并生成 ABAP Unit 测试代码。它使用基于接口的测试替身,并支持预览、写入、激活和运行测试的流程。

主要功能

  1. 01读取类源码、方法和依赖
  2. 02按分支、错误路径和结果设计用例
  3. 03使用接口实现测试替身
  4. 04生成 ABAP Unit 测试类
  5. 05支持代码写入、激活与测试运行

适用场景

  1. 01为 ABAP 类的公开方法生成测试
  2. 02覆盖方法中的分支和异常路径
  3. 03隔离注入依赖以测试类逻辑
  4. 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. 最少数据:使用能够覆盖场景所需的最少值。通常每个测试使用 1-3 条记录。
  2. 直观的值:选择易于直接验证预期结果的值:
    • 对于计算:使用 100.00 - 10.00 = 90.00,而不是 37.83 - 4.27
    • 对于状态检查:使用 IF/CASE 表达式中的确切状态值
    • 对于字符串操作:使用简短且易识别的字符串
  3. 类型正确:严格匹配 ABAP 类型:
    • NUMC 字段使用 '001'(带前导零的字符串)
    • DEC/CURR 字段使用 '100.00'
    • DATS 字段使用 '20260101'
    • ABAP_BOOL 使用 abap_true / abap_false
  4. 确定性:绝不使用 sy-datum、sy-uzeit、cl_abap_context_info=>get_system_date( ) 或随机值
  5. 相互隔离:每个测试方法都应自成一体——setup 方法确保 CUT 状态是全新的
命名约定
  • 测试类:在类的 testclasses include 中使用本地类 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. 报告结果并修复失败项

向用户展示:

  • 通过 / 失败 / 跳过的测试数量
  • 对于任何失败项:断言消息以及失败的测试方法
  • 针对任何失败项的建议修复方案

如果测试失败:

  1. 分析失败消息
  2. 判断问题是由 mock 设置、断言还是 CUT 逻辑引起
  3. 使用方法手术修复具体测试方法:
    SAPWrite(action="edit_method", type="CLAS", name="<class_under_test>", method="<ltc_class>~<failing_method>", source="<fixed_source>")
    
  4. 重新激活并运行

错误处理

常见问题和修复方法
错误原因修复方法
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 步的分析发现没有可测试场景(没有分支、错误路径或有意义的返回值),请提醒用户:“此类没有可供测试的非平凡公共方法。请考虑单元测试是否能为此带来价值。”

评论

支持 Markdown