mcpField
A
abap-unit-testing

abap-unit-testing

github.com/likweitan测试与质量7299 次安装2 周前更新
代码仓库

关于 abap-unit-testing

abap-unit-testing 是编写 ABAP Unit 测试的指南技能,涵盖业务逻辑、CDS、SQL 依赖代码和 RAP 行为。内容包括断言、测试替身、隔离及不同测试环境的做法。

主要功能

  1. 01ABAP Unit 测试类结构
  2. 02断言与 AAA 测试模式
  3. 03依赖注入和测试替身
  4. 04CDS 与 OSQL 测试环境
  5. 05RAP BO 测试替身

适用场景

  1. 01测试业务逻辑和校验规则
  2. 02模拟依赖数据库的代码
  3. 03测试 CDS 视图计算
  4. 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 的测试环境。

工作流

  1. 确定测试目标:

    • 测试类方法中的业务逻辑
    • 测试 CDS 视图实体
    • 测试 RAP BO 行为实现
    • 使用 SQL 测试替身测试依赖数据库的逻辑
    • 为外部依赖设置测试替身
  2. 选择合适的方法:

    • 对纯逻辑(无依赖)进行直接单元测试
    • 通过构造函数或 setter 注入可模拟的依赖
    • 使用 CDS 测试环境测试 CDS 视图
    • 使用 OSQL 测试环境测试依赖 SQL 的代码
    • 使用 RAP BO 测试替身测试 RAP 行为
  3. 遵循 AAA 模式:准备(Arrange)→ 执行(Act)→ 断言(Assert)

  4. 确保测试隔离:测试不得依赖持久化数据或外部系统

测试类基础知识

测试类定义
"! 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.
测试类属性
属性选项用途
DURATIONSHORT / MEDIUM / LONG预计执行时间;SHORT < 1s(CI 默认值)
RISK LEVELHARMLESS / 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_codesy-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 运行时行为

参考资料

相关技能

  • cds-view-entities:用于使用 CDS 测试环境测试 CDS 视图实体
  • rap:用于使用 RAP 测试替身测试 RAP 业务对象
  • clean-abap:用于确保测试代码遵循 Clean ABAP 原则

评论

支持 Markdown