十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ABAP Unit 中用 TDF 模拟 AUTHORITY-CHECK 的完整实践

ABAP Unit 中用 TDF 模拟 AUTHORITY-CHECK 的完整实践 干这行久了你会发现ABAP 项目里凡是要写单元测试的地方AUTHORITY-CHECK基本是默认的“劝退知识点”。权限校验这块逻辑很多团队要么直接跳过要么专门造出一堆测试用户、配一堆 role跑完再靠脚本来清理时间一长没人愿意维护。我一开始也是这么过来的直到有次权限配置在测试环境出了偏差权限分支的 bug 一路漏到生产才彻底下决心把权限校验从“环境依赖”变成“可模拟的依赖”。用 ABAP Unit TDF 来模拟AUTHORITY-CHECK核心思路其实并不复杂让被测代码不再直接执行权限语句而是依赖一个可以替换的接口测试里用替身控制有权限、无权限、异常分支一次跑完所有组合而且每次结果都稳定。这篇文章会把这套方案从重构思路到测试代码完整讲清楚适合已经在写 ABAP Unit 但卡在权限校验场景的开发者也适合刚接触 TDF 想找一个实际落地案例的朋友。1. 为什么AUTHORITY-CHECK是单元测试的天然敌人1.1 权限校验可测性的三个痛点先说说最现实的问题直接用AUTHORITY-CHECK写出来的逻辑为什么进了单元测试就让人头疼。第一个痛点是它是一个语法关键字不是函数也不是方法你没办法在测试里替换它的“实现”。它每次执行时由 SAP 内核拿着当前用户的权限主数据做实时计算结果不是代码能控制的。第二个痛点是结果完全依赖运行环境同一个测试类用开发账号跑可能是sy-subrc 0换个测试账号跑可能就是4环境不一样分支就不一样覆盖率报表上这一段永远飘忽不定。第三个痛点是权限对象往往带好几个字段比如公司代码、工厂、活动类型你无法保证 CI 的执行账号在每个环境都配了完整权限测试就变成了一场“和环境对赌”。很多人第一反应是“那我干脆不测权限分支只测业务逻辑”。但实际上业务逻辑恰恰经常被权限判断包在中间不测权限分支后面的核心代码可能根本进不去。尤其是那些“先查权限、再查数据、最后更新表”的典型流程权限判断就是业务的大门你跳过它等于跳过整个场景。1.2 TDF 的类型边界不是所有依赖都能直接模拟TDFTest Double Framework是 ABAP 自带的依赖替身框架主要入口是cl_abap_testdouble它可以为接口、类、函数模块等创建测试替身并且通过配置来控制替身方法的返回值、抛出异常等。这个框架本身很强但有一个边界经常被忽略TDF 能替换的是那些“可以被调用的对象”——接口方法、类方法、函数模块。AUTHORITY-CHECK是 ABAP 语法层面的指令它不属于任何可调用对象TDF 没法直接“替身”掉。这就是标题里“模拟”二字的来由我们不是真的让 TDF 去替换AUTHORITY-CHECK而是通过重构把这条语句封装到一个方法后面让被测代码依赖一个接口再用 TDF 替掉这个接口。这一步想通了后面所有问题都迎刃而解。1.3 我的总体思路把权限校验“可替换化”整体方案分成两层第一层是生产代码改造把散落在业务逻辑里的AUTHORITY-CHECK收敛到一个权限校验接口的实现类里并且通过一个工厂方法对外提供实例。第二层是测试代码用 TDF 基于接口创建替身在测试里注入替身然后配置不同的返回值覆盖有权限、无权限、权限对象缺失、用户校验失败等分支。这样做的好处不仅仅是可测生产代码里权限规则也被集中管理了以后再改权限逻辑不需要满世界搜AUTHORITY-CHECK语句。2. 先重构把权限校验收敛到可控的依赖边界2.1 定义权限校验接口重构的第一步是定义接口。这只接口参数怎么设计直接影响后续的可维护性。我见过很多人一上来就写一个方法参数做成“字段名-字段值”动态表看起来通用但调用方每次都要拼字符串容易写错且没有语法检查。实际项目里我建议按业务场景拆方法比如“检查事务码权限”、“检查公司代码记账权限”每个方法参数明确调用方读起来一目了然。定义一个简单的权限校验接口示例如下INTERFACE zif_zbc_auth_check PUBLIC. METHODS: check_transaction IMPORTING iv_transaction TYPE tcode RETURNING VALUE(rv_rc) TYPE i, check_company_code IMPORTING iv_bukrs TYPE bukrs iv_actvt TYPE activ_auth RETURNING VALUE(rv_rc) TYPE i. ENDINTERFACE.rv_rc就是sy-subrc的透传值。有人可能会问直接用SY-SUBRC不就行了为什么不省掉一个返回值。这里有两个原因一是 TDF 配置返回值时需要一个明确的方法返回值而不是隐参SY-SUBRC显式返回值便于控制测试断言二是让调用方更明确地“把权限结果当作事务状态来处理”而不是让它依赖一个全局变量降低代码间的隐式耦合。2.2 生产实现类真实AUTHORITY-CHECK只待在这个地方生产实现类就是唯一允许出现AUTHORITY-CHECK语句的地方。类本身逻辑非常简单核心就是封装原生的权限检查并把结果转成返回值。CLASS zcl_zbc_auth_check DEFINITION FINAL CREATE PUBLIC. PUBLIC SECTION. INTERFACES zif_zbc_auth_check. ENDCLASS. CLASS zcl_zbc_auth_check IMPLEMENTATION. METHOD zif_zbc_auth_check~check_transaction. AUTHORITY-CHECK OBJECT S_TCODE ID TCD FIELD iv_transaction. rv_rc sy-subrc. ENDMETHOD. METHOD zif_zbc_auth_check~check_company_code. AUTHORITY-CHECK OBJECT F_BKPF_BUK ID BUKRS FIELD iv_bukrs ID ACTVT FIELD iv_actvt. rv_rc sy-subrc. ENDMETHOD. ENDCLASS.这里有一点要提醒权限对象的字段必须和实际配置保持一致比如S_TCODE的字段名是TCDF_BKPF_BUK的字段名是BUKRS和ACTVT这些写错了即使权限配好了也会一直报没有权限。另外对ACTVT这类活动类型最好由调用方显式传参不要在生产实现里写死否则一个方法只能被单一活动场景使用复用性很差。2.3 工厂方法给测试替身留好“后门”有了接口和生产实现类被测代码还不能直接依赖这个接口实例。如果要让测试在运行时注入替身常见做法有三种构造函数注入、setter 注入、静态工厂注入。在 ABAP 里我推荐静态工厂注入尤其是当被测类里有很多地方都需要权限校验时构造函数参数列表会变得很臃肿而一个全局工厂类可以收敛这个逻辑。工厂类设计如下CLASS zcl_zbc_auth_factory DEFINITION PUBLIC FINAL CREATE PRIVATE. PUBLIC SECTION. CLASS-METHODS: get_auth_check RETURNING VALUE(ro_auth) TYPE REF TO zif_zbc_auth_check, set_auth_check IMPORTING io_auth TYPE REF TO zif_zbc_auth_check. PRIVATE SECTION. CLASS-DATA go_double TYPE REF TO zif_zbc_auth_check. ENDCLASS. CLASS zcl_zbc_auth_factory IMPLEMENTATION. METHOD get_auth_check. ro_auth go_double. IF ro_auth IS NOT BOUND. ro_auth NEW zcl_zbc_auth_check( ). ENDIF. ENDMETHOD. METHOD set_auth_check. go_double io_auth. ENDMETHOD. ENDCLASS.set_auth_check就是给测试留的“后门”。生产环境里没人调用它只要默认没有人调用这个方法go_double就一直是空引用get_auth_check返回真实的zcl_zbc_auth_check实例。测试里调用一次set_auth_check把 TDF 创建的替身塞进去之后被测代码全都从get_auth_check拿到的就是替身了。这种方法的好处是业务类的构造方法不用改也不需要在每个测试用例里一个个替换依赖只替换一次整个测试类里所有用例生效。2.4 重构后被测代码长什么样改造后的被测代码典型的长相是这样CLASS zcl_zbc_order_post DEFINITION PUBLIC FINAL CREATE PUBLIC. PUBLIC SECTION. METHODS: post IMPORTING iv_bukrs TYPE bukrs iv_quantity TYPE menge_d RAISING zcx_zbc_no_authority. ENDCLASS. CLASS zcl_zbc_order_post IMPLEMENTATION. METHOD post. DATA(lo_auth) zcl_zbc_auth_factoryget_auth_check( ). IF lo_auth-check_company_code( iv_bukrs iv_bukrs iv_actvt 01 ) 0. RAISE EXCEPTION TYPE zcx_zbc_no_authority. ENDIF. 后续业务逻辑例如保存订单 ENDMETHOD. ENDCLASS.注意重构后的业务代码唯一依赖的是zcl_zbc_auth_factoryget_auth_check这个“静态接口”它返回的具体对象是真实类还是测试替身业务代码根本不关心。这种设计其实就是把权限校验当成了一个“外部依赖”而非“内部过程”为 TDF 的注入打好了基础。3. 在 ABAP Unit 里引入 TDF建替身、配行为、跑用例3.1 创建替身并注入到被测代码在测试类里第一步是创建替身。TDF 创建替身的入口是cl_abap_testdoublecreate传入要替身的接口名或类名然后通过CAST来转换成目标接口类型。这里强烈建议基于接口创建替身而不是基于具体类因为 TDF 对具体类的替身有更多限制比如 final class 无法被替身而接口则是最灵活的情况。测试代码的 SETUP 部分可以这样写CLASS ltcl_test DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. PRIVATE SECTION. DATA: mo_auth_double TYPE REF TO zif_zbc_auth_check, mo_cut TYPE REF TO zcl_zbc_factory. METHODS: setup, 权限通过 post_authorized FOR TESTING, 权限拒绝 post_unauthorized FOR TESTING. ENDCLASS. CLASS ltcl_test IMPLEMENTATION. METHOD setup. mo_auth_double CAST zif_zbc_auth_check( cl_abap_testdoublecreate( ZIF_ZBC_AUTH_CHECK ) ). zcl_zbc_auth_factoryset_auth_check( mo_auth_double ). mo_cut NEW zcl_zbc_factory( ). ENDMETHOD. ENDCLASS.这样每个测试用例跑之前都会先执行 setup工厂里的引用已经被替换成替身。需要注意如果测试类里处理的是对静态工厂的注入那么每个测试用例之间工厂里那个go_double值并不会自动清掉。好在这里的替身实例在 setup 里会被重新赋值覆盖所以不会出现上个用例的状态残留。但如果一个开发类里有多个测试类都用同一个工厂那就要在teardown里恢复工厂状态否则测试类和测试类之间会互相污染。3.2 配置有权限和无权限两个分支TDF 的核心能力就是用configure_call来配置替身方法的返回值。以权限校验为例你要测“有权限”这条分支就让替身返回0要测“无权限”分支就让替身返回4。配置之后还需要真正调用一次方法TDF 才会把你配置的行为和那个方法调用“绑定”起来。具体写法如下METHOD post_authorized. cl_abap_testdoubleconfigure_call( mo_auth_double )-returning( 0 ). mo_auth_double-check_company_code( iv_bukrs 1000 iv_actvt 01 ). ENDMETHOD.TDF 的匹配逻辑是配置行为时它会根据调用参数来决定当前配置生效的范围。如果在configure_call后面跟着的方法调用里传了具体参数那么只有参数完全一致的调用才会命中这个返回值。如果想让替身对所有参数都返回同一个值需要在调用里不指定参数或者使用ignore_all_parameters( )。这一步非常容易踩坑后面专门说。无权限分支就是配置返回4或者其它非零值METHOD post_unauthorized. cl_abap_testdoubleconfigure_call( mo_auth_double )-returning( 4 ). mo_auth_double-check_company_code( iv_bukrs 1000 iv_actvt 01 ). ENDMETHOD.如果你要测异常抛出的逻辑那就不配returning而是配置raisecl_abap_testdoubleconfigure_call( mo_auth_double )-raise( lo_exception ).这种方式更多用于模拟权限检查函数本身抛异常的场景。真实业务里AUTHORITY-CHECK不太会抛异常但如果你把权限校验实现类里加入了其他数据读取比如去配置表里取权限对象字段映射这个异常分支就有测试价值了。3.3 完整测试类示例与结果断言把上面的片段组装成一个完整测试类测试逻辑就非常清晰了CLASS ltcl_test DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. PRIVATE SECTION. DATA: mo_auth_double TYPE REF TO zif_zbc_auth_check, mo_cut TYPE REF TO zcl_zbc_order_post. METHODS: setup, authorized_post FOR TESTING, unauthorized_raises FOR TESTING. ENDCLASS. CLASS ltcl_test IMPLEMENTATION. METHOD setup. mo_auth_double CAST zif_zbc_auth_check( cl_abap_testdoublecreate( ZIF_ZBC_AUTH_CHECK ) ). zcl_zbc_auth_factoryset_auth_check( mo_auth_double ). mo_cut NEW zcl_zbc_order_post( ). ENDMETHOD. METHOD authorized_post. cl_abap_testdoubleconfigure_call( mo_auth_double )-returning( 0 ). mo_auth_double-check_company_code( iv_bukrs 1000 iv_actvt 01 ). mo_cut-post( iv_bukrs 1000 iv_quantity 10 ). cl_abap_unit_assertassert_subrc( ). ENDMETHOD. METHOD unauthorized_raises. cl_abap_testdoubleconfigure_call( mo_auth_double )-returning( 4 ). mo_auth_double-check_company_code( iv_bukrs 1000 iv_actvt 01 ). TRY. mo_cut-post( iv_bukrs 1000 iv_quantity 10 ). cl_abap_unit_assertfail( should have raised ). CATCH zcx_zbc_no_authority. expected ENDTRY. ENDMETHOD. ENDCLASS.这里有权用例里业务逻辑真正会执行到后续逻辑所以最终用assert_subrc去判断“没有异常地正常完成”。无权限用例则用 TRY-CATCH 来验证异常被抛出。这里有个细节不要在post跑完之后才断言返回值因为被测方法已经把异常抛出来了后续代码根本不会执行。所以要么在异常分支用 TRY-CATCH 包住整个调用要么用catch加断言。3.4 补充TEST-INJECTION 与 TDF 的对比有些老项目没有使用 TDF用的是 ABAP 里比较早期的可测试性注入手段测试代码里用TEST-INJECTION配合生产代码里的TEST-SEAM去覆盖逻辑片段。这套机制也能“模拟”权限校验把真实AUTHORITY-CHECK所在的代码段替换成测试分支但它的弱点也很明显生产代码里到处要留TEST-SEAM口子业务代码会被测试代码污染而且一个 seam 只能做一种替换想配多种返回值就得写多种测试类。TDF 则完全绕开了这个思路它替身的是“对象”而不是“代码片段”行为可以按调用配置灵活得多也不用在生产代码里留奇怪的测试开关。我的建议是新项目直接用 TDF老项目如果只有零星几处TEST-SEAM短期可以保留但新写的可测试性代码建议统一走到 TDF 这套标准上来。4. 我在实践中排查过的常见问题4.1 工厂替换没生效替身明明配了却没被调用这个问题最典型的表现是测试类里跑了zcl_zbc_auth_factoryset_auth_check( mo_auth_double )但被测代码里走的还是真实权限检查结果测试跑出来还是依赖当前用户权限。排查思路其实就两步第一步看被测代码里有没有经过工厂类拿实例是否只是局部NEW了一个真实实现类或者直接在一个私有方法里又写了一遍AUTHORITY-CHECK。第二步看工厂里go_double是否在某处被重设比如测试类的 setup 顺序不对或者上一个测试类的 teardown 把工厂重置了。另外一个容易被忽略的问题工厂类里如果get_auth_check用NEW zcl_zbc_auth_check( )创建真实对象然后又赋值给ro_auth在测试里其实没有影响因为只要go_double有值真实的NEW根本不会执行。但如果set_auth_check传入的是空引用工厂会“误判”测试没注入替身又把真实实现类创建出来了。所以测试里要确认替身对象真的被CL_ABAP_TESTDOUBLECREATE创建成功不是空引用。4.2 SY-SUBRC 的全局污染AUTHORITY-CHECK执行之后结果写在SY-SUBRC里。如果你在生产实现里没有把这个值传递给一个局部返回变量而是直接依赖调用方后续再判断SY-SUBRC那测试里就很尴尬TDF 替身根本没有执行AUTHORITY-CHECK自然也不会改SY-SUBRC调用方读取会读到上次任何一句成功语句留下的0或者垃圾值结果完全随机。所以封装实现类里必须有一行rv_rc sy-subrc。这个坑我踩过一次排查了一个下午才发现是旧代码里直接把SY-SUBRC当接口返回值用因为普通运行环境下权限检查刚刚执行过侥幸没出问题但一旦改成 TDF 就立刻爆炸。另外一点测试代码中断言完成后SY-SUBRC也被各种语句改来改去所以如果要断言权限返回值别在post之后用CL_ABAP_UNIT_ASSERTASSERT_EQUALS( act sy-subrc )而是应该捕获业务异常或者在业务代码里把权限校验失败时的返回值包装到异常里再断言。这样测试才稳定。4.3 替身返回值和真实AUTHORITY-CHECK行为不一致TDF 的替身是手工配置返回值的它不会真的去检查权限对象、权限字段也不会真的读取用户主数据。这个特性既是优点也是风险如果你在配置替身时只返回了0和4但真实权限校验还可能出现12、24这类返回值它们代表不同含义比如字段不完整、对象不存在那么业务代码里如果有基于这些返回值的特殊分支就会漏测。建议的做法是在测试类里把返回值当成一个“枚举”来处理每遇到一个新的AUTHORITY-CHECK返回值先去看生产代码的分支有没有覆盖到没有覆盖就补一个测试用例。还可以在权限校验实现类里加一个静态方法专门把SY-SUBRC转成一个带语义的枚举比如authorized、not_authorized、invalid_object这样测试代码和生产代码共用同一套语义替身配置时就不会只填一个魔法数。4.4 测试类之间的静态工厂状态残留静态工厂的go_double是整个程序都可见的全局状态。如果一个项目里有多个测试类都针对同一个工厂类做注入它们之间可能会互相干扰。比如 A 测试类跑完把替身留在了工厂里B 测试类 setup 阶段忘了重新注入就会直接用到 A 的替身然后怎么排查都看不出问题。最稳妥的处理方法是在每个测试类的TEARDOWN方法里调用一次zcl_zbc_auth_factoryset_auth_check( EXPORTING io_auth VALUE #( ) )把工厂状态清掉。或者直接把set_auth_check改成更明确的reset_auth_check避免误用。这属于一个很小的代码卫生习惯但对 CI 跑测试的稳定性影响非常大。常见问题可能原因解决办法替身没生效还是走真实权限工厂类未注入或注入后又被重置检查set_auth_check调用顺序teardown 里恢复状态SY-SUBRC断言不稳定生产代码没透传返回值测试受全局变量影响实现类里执行rv_rc sy-subrc断言用捕获异常日志或覆盖率显示有分支没测只配了0和4遗漏其它返回值把SY-SUBRC映射成语义枚举补齐边界用例TDF 报方法参数匹配不上configure_call后调用参数和实际调用不一致使用方法参数匹配或ignore_all_parameters5. 最后说几点实际操作中的体会这套方案我已经在几个项目里落地过整体收益比想象中要大。以前权限评审阶段开发人员最怕的就是“改一行权限配置所有依赖这个权限的业务逻辑全要手工回归一遍”。现在权限分支全部进了单元测试CI 上跑一遍有没有权限、权限边界对不对几分钟就能看到结果。我个人实际跑下来最大的感受是重构比测什么更重要。如果你不把散落的AUTHORITY-CHECK收敛起来再强的 TDF 也帮不了你但一旦收敛了权限校验就从一个“到处都在发生的环境状态”变成了“一个可替换的接口”后面加权限、拆权限、做权限模拟都顺手很多。另外一个小技巧是给权限校验类做好语义化封装把SY-SUBRC的魔数封装成常量测试代码读起来会舒服很多比如IF_RC_AUTHORIZED、IF_RC_NOT_AUTHORIZED这样测试用例里一眼就能看懂这个用例在测什么场景。最后再提一句TDF 不是银弹它解决的是“可重复性”问题最终权限配置本身的正确性还是需要真正的权限评审和配置验证流程来兜底。但至少从单元测试这个层面“权限校验没法测”这个说法以后真的可以翻篇了。
返回列表