关于 abap-unit-testing
abap-unit-testing 是编写 ABAP Unit 测试的指南技能,涵盖业务逻辑、CDS、SQL 依赖代码和 RAP 行为。内容包括断言、测试替身、隔离及不同测试环境的做法。
主要功能
- 01ABAP Unit 测试类结构
- 02断言与 AAA 测试模式
- 03依赖注入和测试替身
- 04CDS 与 OSQL 测试环境
- 05RAP BO 测试替身
适用场景
- 01测试业务逻辑和校验规则
- 02模拟依赖数据库的代码
- 03测试 CDS 视图计算
- 04验证 RAP 行为实现
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ likweitan/ abap-skills --skill abap-unit-testing安装后在 Claude Code 中输入/abap-unit-testing确认已加载。
附带文件references/test-environment-examples.md
详细介绍
ABAP 单元测试
编写高效 ABAP Unit 测试的指南,涵盖测试类设置、断言、测试替身、模拟框架,以及 CDS、SQL 和 RAP 的测试环境。
工作流
-
确定测试目标:
- 测试类方法中的业务逻辑
- 测试 CDS 视图实体
- 测试 RAP BO 行为实现
- 使用 SQL 测试替身测试依赖数据库的逻辑
- 为外部依赖设置测试替身
-
选择合适的方法:
- 对纯逻辑(无依赖)进行直接单元测试
- 通过构造函数或 setter 注入可模拟的依赖
- 使用 CDS 测试环境测试 CDS 视图
- 使用 OSQL 测试环境测试依赖 SQL 的代码
- 使用 RAP BO 测试替身测试 RAP 行为
-
遵循 AAA 模式:准备(Arrange)→ 执行(Act)→ 断言(Assert)
-
确保测试隔离:测试不得依赖持久化数据或外部系统
测试类基础知识
测试类定义
"! Test class for ZCL_MY_CLASS
CLASS ltc_my_class DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA cut TYPE REF TO zcl_my_class. "Class Under Test
CLASS-METHODS class_setup. "Once before all tests
CLASS-METHODS class_teardown. "Once after all tests
METHODS setup. "Before each test
METHODS teardown. "After each test
METHODS test_calculate_total FOR TESTING.
METHODS test_validate_input FOR TESTING.
METHODS test_empty_input FOR TESTING RAISING cx_static_check.
ENDCLASS.
CLASS ltc_my_class IMPLEMENTATION.
METHOD class_setup.
" One-time setup for all tests in this class
ENDMETHOD.
METHOD class_teardown.
" One-time cleanup
ENDMETHOD.
METHOD setup.
" Create fresh instance before each test
cut = NEW #( ).
ENDMETHOD.
METHOD teardown.
" Cleanup after each test
ENDMETHOD.
METHOD test_calculate_total.
" Arrange
DATA(lv_quantity) = 5.
DATA(lv_price) = CONV decfloat34( '10.50' ).
" Act
DATA(lv_result) = cut->calculate_total(
iv_quantity = lv_quantity
iv_price = lv_price ).
" Assert
cl_abap_unit_assert=>assert_equals(
act = lv_result
exp = CONV decfloat34( '52.50' )
msg = 'Total should be quantity * price' ).
ENDMETHOD.
METHOD test_validate_input.
cl_abap_unit_assert=>assert_true(
act = cut->validate_input( 'VALID_INPUT' )
msg = 'Valid input should return true' ).
ENDMETHOD.
METHOD test_empty_input.
TRY.
cut->validate_input( '' ).
cl_abap_unit_assert=>fail( msg = 'Should have raised exception' ).
CATCH zcx_validation_error INTO DATA(lx_error).
cl_abap_unit_assert=>assert_bound(
act = lx_error
msg = 'Exception should be raised for empty input' ).
ENDTRY.
ENDMETHOD.
ENDCLASS.
测试类属性
| 属性 | 选项 | 用途 |
|---|---|---|
DURATION | SHORT / MEDIUM / LONG | 预计执行时间;SHORT < 1s(CI 默认值) |
RISK LEVEL | HARMLESS / DANGEROUS / CRITICAL | 对系统数据的影响;HARMLESS = 不修改数据库 |
测试方法附加项
| 附加项 | 用途 |
|---|---|
FOR TESTING | 将方法标记为测试方法 |
RAISING cx_static_check | 允许异常继续传播(未处理的异常会导致测试失败) |
CL_ABAP_UNIT_ASSERT — 断言方法
| 方法 | 用途 | 示例 |
|---|---|---|
assert_equals | 值相等 | assert_equals( act = result exp = 42 ) |
assert_true | 布尔值为真 | assert_true( act = lv_flag ) |
assert_false | 布尔值为假 | assert_false( act = lv_flag ) |
assert_initial | 值为初始值 | assert_initial( act = lt_table ) |
assert_not_initial | 值不为初始值 | assert_not_initial( act = lt_result ) |
assert_bound | 引用已绑定 | assert_bound( act = lo_instance ) |
assert_not_bound | 引用未绑定 | assert_not_bound( act = lo_ref ) |
assert_differs | 值不相同 | assert_differs( act = val1 exp = val2 ) |
assert_char_cp | 字符匹配模式 | assert_char_cp( act = lv_text exp = '*error*' ) |
assert_char_np | 字符不匹配模式 | assert_char_np( act = lv_text exp = '*secret*' ) |
assert_number_between | 数字在范围内 | assert_number_between( number = val lower = 1 upper = 10 ) |
assert_table_contains | 表包含行 | assert_table_contains( line = wa table = lt_result ) |
assert_table_not_contains | 表不包含行 | assert_table_not_contains( line = wa table = lt_result ) |
assert_return_code | sy-subrc 检查 | assert_return_code( act = sy-subrc exp = 0 ) |
fail | 强制测试失败 | fail( msg = 'Should not reach here' ) |
常见断言模式
" Check table has expected number of entries
cl_abap_unit_assert=>assert_equals(
act = lines( lt_result )
exp = 3
msg = 'Expected 3 result entries' ).
" Check exception message
TRY.
cut->some_method( ).
cl_abap_unit_assert=>fail( msg = 'Expected exception' ).
CATCH zcx_my_exception INTO DATA(lx).
cl_abap_unit_assert=>assert_equals(
act = lx->get_text( )
exp = 'Expected error message' ).
ENDTRY.
" Check that table contains a specific key
cl_abap_unit_assert=>assert_table_contains(
line = VALUE zstructure( key_field = 'ABC' )
table = lt_result
msg = 'Result should contain entry ABC' ).
依赖注入与测试替身
构造函数注入模式
" Production interface
INTERFACE zif_data_provider.
METHODS get_data
RETURNING VALUE(rt_data) TYPE ztab_data.
ENDINTERFACE.
" Production class with injectable dependency
CLASS zcl_processor DEFINITION.
PUBLIC SECTION.
METHODS constructor
IMPORTING io_provider TYPE REF TO zif_data_provider OPTIONAL.
METHODS process
RETURNING VALUE(rv_result) TYPE string.
PRIVATE SECTION.
DATA mo_provider TYPE REF TO zif_data_provider.
ENDCLASS.
CLASS zcl_processor IMPLEMENTATION.
METHOD constructor.
mo_provider = COND #(
WHEN io_provider IS BOUND THEN io_provider
ELSE NEW zcl_default_provider( ) ).
ENDMETHOD.
METHOD process.
DATA(lt_data) = mo_provider->get_data( ).
" Process data...
ENDMETHOD.
ENDCLASS.
测试替身(手动 Mock)
" Test double implementing the interface
CLASS ltd_data_provider DEFINITION FOR TESTING.
PUBLIC SECTION.
INTERFACES zif_data_provider.
DATA mt_test_data TYPE ztab_data.
ENDCLASS.
CLASS ltd_data_provider IMPLEMENTATION.
METHOD zif_data_provider~get_data.
rt_data = mt_test_data.
ENDMETHOD.
ENDCLASS.
" Test class using the double
CLASS ltc_processor DEFINITION FINAL FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA cut TYPE REF TO zcl_processor.
DATA mo_provider TYPE REF TO ltd_data_provider.
METHODS setup.
METHODS test_process_with_data FOR TESTING.
ENDCLASS.
CLASS ltc_processor IMPLEMENTATION.
METHOD setup.
mo_provider = NEW #( ).
cut = NEW #( io_provider = mo_provider ).
ENDMETHOD.
METHOD test_process_with_data.
" Arrange — configure test double
mo_provider->mt_test_data = VALUE #(
( key = '1' value = 'A' )
( key = '2' value = 'B' ) ).
" Act
DATA(lv_result) = cut->process( ).
" Assert
cl_abap_unit_assert=>assert_not_initial( act = lv_result ).
ENDMETHOD.
ENDCLASS.
CDS 测试环境
使用 cl_cds_test_environment=>create( i_for_entity = 'ZI_ENTITY' ) 为 CDS 视图的所有数据源创建存根。使用 insert_test_data( ) 插入测试数据,在 setup 中使用 clear_doubles( ) 清除数据,并在 class_teardown 中调用 destroy( )。
OSQL(Open SQL)测试环境
使用 cl_osql_test_environment=>create( i_dependency_list = ... ) 为测试依赖 SQL 的类创建特定数据库表/视图的存根。生命周期模式与 CDS 测试环境相同。
RAP BO 测试替身
- 事务缓冲区替身(
cl_botd_txbufdbl_bo_test_env):用于测试通过 EML 使用 RAP BO 的代码 - Mock EML API(
cl_botd_mockemlapi_bo_test_env):通过配置 Mock 响应,测试 RAP 处理程序方法的实现
测试接缝(遗留代码)
在生产代码中使用 TEST-SEAM / END-TEST-SEAM,并在测试中使用 TEST-INJECTION / END-TEST-INJECTION。新代码优先使用构造函数注入。
如需以上所有内容的完整代码示例,请阅读 references/test-environment-examples.md。
最佳实践
测试设计
- 每个测试只验证一种断言概念——每个测试应验证一种行为
- 测试方法名称应清晰描述用途——使用
test_reject_negative_quantity,而不是test_1 - 测试彼此独立——任何测试都不应依赖其他测试的结果或执行顺序
- 保持测试快速——使用
DURATION SHORT,避免不必要的准备工作
测试隔离
- 始终使用测试替身替代外部依赖(数据库、API、其他业务对象)
- 使用
setup/teardown确保状态干净 - 对于开销较大的一次性设置(测试环境),使用
class_setup/class_teardown - 对于测试环境类,始终在
setup中调用clear_doubles( ) - 始终在
class_teardown中调用destroy( )
测试结构
- 将测试类放在本地测试包含文件中(ADT 中的测试类选项卡)
- 测试替身的前缀使用
ltd_(本地测试替身) - 测试类的前缀使用
ltc_(本地测试类) - 将相关测试归入同一个测试类
测试内容
- 业务逻辑和计算
- 验证规则和错误场景
- 边界情况(空输入、边界值、空引用)
- CDS 视图计算和聚合
- RAP 处理程序逻辑(操作、验证、判定)
不要测试的内容
- 框架提供的功能(RAP 中的托管 CRUD)
- 简单的 getter/setter 方法
- ABAP 运行时行为
参考资料
- SAP ABAP 速查表 — ABAP 单元测试
- SAP 帮助 — ABAP 单元测试
- SAP 帮助 — CDS 测试替身框架
- SAP 帮助 — RAP 业务对象测试替身
- Clean ABAP — 测试
相关技能
- cds-view-entities:用于使用 CDS 测试环境测试 CDS 视图实体
- rap:用于使用 RAP 测试替身测试 RAP 业务对象
- clean-abap:用于确保测试代码遵循 Clean ABAP 原则