1. 项目背景与核心价值
做SAP开发的朋友,迟早都会撞上这么一堵墙——周边系统越来越多,订单、物流、支付、主数据,各种业务系统都在往外吐接口。你这边还在用RFC、IDoc跟老伙计们打交道,人家隔壁团队新上的平台却只认RESTful API。怎么办?硬着头皮学呗。
这个项目的核心,就是在SAP ABAP环境里,写一套能稳定调用RESTful API的测试用例。说白了,就是用ABAP这门老语言,去敲新一代接口的门。我把它做成了一个轻量级的可复用框架,不依赖外部工具,直接在SE38里跑,既能测通GET/POST/PUT/DELETE这些基础动作,也能处理鉴权、超时、报文解析这些绕不开的细节。
这个内容适合谁看?两类人。一类是刚接触接口开发的ABAP工程师,想找个能直接抄的作业;另一类是做系统集成的顾问,需要验证SAP跟外部平台的连通性,但又不想每次都拉上Java或者Python同事帮忙。我自己就是从这个坑里爬出来的,所以把这套东西整理成文,希望能帮你少走点弯路。
项目核心解决的痛点很明确:一是ABAP原生语法对RESTful支持得比较别扭,二是调试HTTP接口时报文乱、报错玄学,三是常规文档只讲成功路径,不讲失败怎么查。所以这套测试用例设计的重点,不在于把代码写得多花哨,而在于把调用链路的每一步都拆开、看清楚,出了问题能精准定位是网络、鉴权、报文还是对端系统的问题。
2. 整体设计与思路拆解
2.1 为什么不用类CL_HTTP_CLIENT
很多ABAP老手第一反应是,SAP不是自带IF_HTTP_CLIENT嘛,直接用它调不就完了。没错,IF_HTTP_CLIENT确实能用,但它有几个很实际的问题。
首先是版本差异大。在老一点的ECC版本上,IF_HTTP_CLIENT的参数传递方式、响应读取方式跟S/4 HANA上有不少差别,你一段代码抄过来,可能编译都过不了。其次是对RESTful语义的支持很弱,比如HTTP状态码判断、Header操作、JSON解析,都得自己手工拼。最要命的是,老代码里充斥着被标记为过时的METHOD,每次升级都要跟着改,维护成本很高。
所以我选择了另一条路:直接用IF_HTTP_CLIENT的增强玩法——结合系统标准类CL_HTTP_CLIENT来构建请求,同时自己封装一层Json解析器。这套方案的好处是,底层调用还是SAP标准能力,稳定性有保障;上层封装完全由自己控制,接口统一,测试用例写起来顺手,参数调整灵活。
2.2 测试用例框架的分层设计
这套测试用例没有走复杂的BDD或者TDD路线,而是采用了简单粗暴但非常实用的三层设计:
- 第一层:基础工具类,负责HTTP请求的发送、响应接收、状态码提取;
- 第二层:业务接口封装,每个外部接口对应一个方法,把URL、Header、Body的组装细节藏起来;
- 第三层:测试用例主体,按业务场景组织,一个场景一个REPORT或一个FORM。
这样分层的好处是,底层改动不影响业务层,比如超时时间从5秒改成15秒,只需要动工具类;新增接口也不会污染已有用例,复制一个方法改改参数就行。实测下来,维护成本比把所有逻辑堆在一个REPORT里低了不是一星半点。
2.3 为什么强调"测试用例"而不仅是"调用代码"
项目标题里特意写了"测试用例"三个字,这是有讲究的。因为我们在SAP里调外部接口,最大的痛苦不是你调不通,而是你调通了,但不知道它什么时候会断、断在哪里。普通调用代码只能告诉你"成功"或"失败",而一套好的测试用例能告诉你"失败在哪一步、为什么失败、对端返回了什么"。
我的做法是,每个测试用例不仅输出最终结果,还会把请求报文、响应报文、耗时、HTTP状态码、错误码全部记录到内存表里,最后统一打印或者写入日志表。这样即便接口出问题,你也能拿着完整链路去跟对端系统对质,不用猜。
3. 核心细节解析与实操要点
3.1 HTTP客户端创建的三种方式与选择
在ABAP里创建一个HTTP客户端,常规有三种做法,我分别踩过坑,说下结论。
第一种是直接NEW一个CL_HTTP_CLIENT实例。这种方式最简单,但它不会自动带上SAP的代理配置,也不绑定目的地,适合纯内网直连的场景。如果你要测试的环境不需要走代理,用它最省事。
第二种是通过CL_HTTP_CLIENT_CREATE_BY_DESTINATION创建,绑定一个SM59配置的逻辑目的地。这种方式最推荐,因为代理、超时、SSL配置都可以在SM59里统一管理,测试用例代码里不用写死任何网络参数。切换环境的时候,只改SM59指向就行。
第三种是CL_HTTP_CLIENT_CREATE_BY_CONFIGURATION,基于配置文件创建,适合有统一HTTP配置管理的团队。但这个在多数项目里用得不多,因为大部分ABAP环境还没有引入这套配置体系。
我实际选的是第二种。原因很朴素:项目上要连的外部环境不止一个,测试环境、预发环境、生产环境各有一套地址,用SM59管理,代码零改动,只调配置,干净利落。
3.2 Header与Content-Type的细节处理
RESTful API对Header的要求往往比SOAP严格得多。最常见的一个坑是Content-Type没设对,对端直接给你返回415或者400。我见过很多新手在这上面卡半天。
一般GET请求需要设置:
lv_header_name = 'Content-Type'. lv_header_value = 'application/json'. o_client->request->set_header_field( EXPORTING name = lv_header_name value = lv_header_value ).但POST、PUT请求,光设Content-Type还不够,很多对端还要校验Accept头。我的建议是统一把这两个都设置上,避免对端返回内容格式跟预期不符。
另外,如果你的接口需要传Token,常见的位置有两种:一种是放在Authorization头里,比如Bearer Token;一种是放在自定义头里,比如X-Access-Token。这两种在代码里都是同样的set_header_field操作,但Token从哪来,就得看你项目的鉴权方式了。后面我会单独讲鉴权这块。
3.3 响应处理的三大关键点
收到响应之后,不要急着解析Body,先做三件事。
第一步是看状态码。2xx是成功,4xx基本是请求报文有问题,5xx是对端服务异常。但很多接口返回HTTP 200,业务上却是失败的,比如返回码是5001之类的错误码。所以单看HTTP状态码是远远不够的,还要解析Body里的业务状态。
第二步是取响应Body。注意响应可能是压缩的,如果对端返回了Content-Encoding: gzip,而你的HTTP客户端没做解压处理,拿到的就是乱码。这时候要么让对端关闭压缩,要么在请求头里明确声明不接受压缩格式。我项目里有个接口就是用gzip返回的,折腾了半天才发现是这个原因。
第三步是记录响应耗时。在发送请求前记一下时间戳,收到响应后算差值,这个数据非常有用。比如某个接口平时50毫秒,突然变成3秒,那基本可以断定是对端出问题了。
4. 实操过程与核心环节实现
4.1 环境准备与基础工具类实现
我的开发环境是S/4 HANA 1709版本,ABAP版本为7.51。这套代码理论上在ECC 6.0 EHP7以上都能跑,但老版本的话需要手动确认类是否存在。
先建一个工具类,取名ZCL_REST_CLIENT,用来封装HTTP调用逻辑。核心方法就是一个,叫SEND_REQUEST,入参是URL、Method、Header表、Body字符串,出参是响应Body和状态码。
CLASS zcl_rest_client DEFINITION PUBLIC FINAL CREATE PUBLIC. PUBLIC SECTION. TYPES: BEGIN OF ty_header, name TYPE string, value TYPE string, END OF ty_header, ty_header_tab TYPE TABLE OF ty_header. METHODS: send_request IMPORTING iv_url TYPE string iv_method TYPE string it_header TYPE ty_header_tab OPTIONAL iv_body TYPE string OPTIONAL EXPORTING ev_response TYPE string ev_status_code TYPE i ev_status_text TYPE string ev_error_text TYPE string RAISING cx_root. ENDCLASS.实际实现里,发送请求的核心代码是:
METHOD send_request. DATA: lo_client TYPE REF TO if_http_client, lv_url TYPE string, lv_method TYPE string, lv_body TYPE string, ls_header LIKE LINE OF it_header. lv_url = iv_url. lv_method = iv_method. lv_body = iv_body. " 这里使用SM59里配置的目的地,我这里取名ZEXTERNAL_API cl_http_client=>create_by_destination( EXPORTING destination = 'ZEXTERNAL_API' IMPORTING client = lo_client EXCEPTIONS argument_not_found = 1 destination_not_found = 2 internal_error = 3 OTHERS = 4 ). IF sy-subrc <> 0. ev_error_text = 'HTTP客户端创建失败,请检查SM59配置'. RETURN. ENDIF. " 设置请求方法 lo_client->request->set_method( lv_method ). " 设置请求URL lo_client->request->set_header_field( EXPORTING name = '~request_uri' value = lv_url ). " 设置请求Header LOOP AT it_header INTO ls_header. lo_client->request->set_header_field( EXPORTING name = ls_header-name value = ls_header-value ). ENDLOOP. " 如果请求体不为空,设置请求体内容 IF lv_body IS NOT INITIAL. lo_client->request->set_cdata( lv_body ). ENDIF. " 发送请求 lo_client->send( EXCEPTIONS http_communication_failure = 1 http_invalid_state = 2 http_processing_failed = 3 OTHERS = 4 ). IF sy-subrc <> 0. " 通信异常时,从客户端里取错误信息 ev_error_text = lo_client->get_last_error( ). RETURN. ENDIF. " 接收响应 lo_client->receive( EXCEPTIONS http_communication_failure = 1 http_invalid_state = 2 http_processing_failed = 3 OTHERS = 4 ). IF sy-subrc <> 0. ev_error_text = lo_client->get_last_error( ). RETURN. ENDIF. " 提取状态码 ev_status_code = lo_client->response->get_status( ). ev_status_text = lo_client->response->get_status_text( ). " 提取响应内容 ev_response = lo_client->response->get_cdata( ). " 关闭连接 lo_client->close( ). ENDMETHOD.这段代码看起来简单,但有几个细节需要特别注意。
第一,~request_uri这个Header名不要拼错,很多教程里根本没提这个,导致URL设置无效,对端一直收到空路径。第二,创建HTTP客户端时的EXCEPTIONS一定要写全,否则异常抛出来你的程序直接崩溃,连排查的机会都没有。第三,每次调用完记得CLOSE,否则连接池会被占满,我生产上遇到过SAP应用服务器HTTP连接数被打满的情况,就是这个原因。
4.2 GET请求测试用例完整演示
有了工具类,写一个GET请求的测试就非常顺手了。我拿一个模拟的外部工单查询接口来演示。
REPORT ztest_rest_get. DATA: lo_client TYPE REF TO zcl_rest_client, lt_header TYPE zcl_rest_client=>ty_header_tab, ls_header LIKE LINE OF lt_header, lv_resp TYPE string, lv_status TYPE i, lv_stext TYPE string, lv_err TYPE string. START-OF-SELECTION. CREATE OBJECT lo_client. " 设置Header头 ls_header-name = 'Content-Type'. ls_header-value = 'application/json'. APPEND ls_header TO lt_header. ls_header-name = 'Accept'. ls_header-value = 'application/json'. APPEND ls_header TO lt_header. " 如果有Token,取消下面注释并按实际调整 " ls_header-name = 'Authorization'. " ls_header-value = 'Bearer xxxxxxx'. " APPEND ls_header TO lt_header. " 调用GET接口 lo_client->send_request( EXPORTING iv_url = '/api/workorder/query' iv_method = 'GET' it_header = lt_header IMPORTING ev_response = lv_resp ev_status_code = lv_status ev_status_text = lv_stext ev_error_text = lv_err ). " 输出结果 WRITE: / 'HTTP状态码:', lv_status. WRITE: / '状态文本:', lv_stext. IF lv_err IS NOT INITIAL. WRITE: / '通信错误:', lv_err. ELSE. WRITE: / '响应报文:', lv_resp. ENDIF.这里有一个很重要的设计:URL我写的是相对路径,不带IP和端口。因为IP和端口已经配置在SM59的目的地里了,代码里写相对路径有两个好处。一是测试用例代码可以在任何环境直接跑,不用改地址;二是代码里不会暴露环境信息,项目上做代码传输的时候就少了一道脱敏的麻烦。
4.3 POST请求与请求体构造
POST请求比GET多一个动作,就是设置请求体。但请求体的构造,恰恰是重灾区。
外部系统的接口定义五花八门,有的要求根节点是对象,有的是数组,有的字段命名是下划线风格,有的是驼峰。我的建议是,在测试用例里先用字符串拼接出一个最小可用请求体,跑通了之后再考虑用JSON转换工具动态构造。
下面是一段实际可用的POST请求体构造代码:
DATA: lv_body TYPE string. CONCATENATE '{' '"orderNo":"SO-20240815-001",' '"customerCode":"C001",' '"itemList":[' '{' '"materialCode":"MAT001",' '"quantity":2,' '"unitPrice":99.5' '},' '{' '"materialCode":"MAT002",' '"quantity":1,' '"unitPrice":59' '}' ']' '}' INTO lv_body.用CONCATENATE拼字符串是笨办法,但胜在直观。新手调试阶段用这种方式,可以准确知道发送出去的报文长什么样。等接口稳定了,再用/data/cl_json或者/UI2/CL_JSON把内部表转JSON,能写得更优雅,但出错时反而不容易定位。
POST请求发送代码和GET基本一样,只是Method换成POST,然后把lv_body传给IV_BODY参数。但这里有一个容易踩的坑:如果接口返回201 Created,而你的工具类只是判断状态码是否等于200,就会误判为失败。所以工具类里对状态码的判断,建议放宽到2xx才算成功,不要死等200。
4.4 鉴权流程的四种常见处理方式
外部接口的认证五花八门,我梳理了一下,ABAP调用RESTful API常见的鉴权方式基本是四种,你们对号入座。
第一种是Basic Auth,最简单,用户名密码冒号连接后做Base64编码,放进Authorization头。这种方式在ABAP里用字符串拼接加编码就能搞定,不需要额外类库。
第二种是Token模式,先调一个登录接口获取Token,然后把它缓存起来,后续请求都带着它。ABAP里可以用类CL_MEMORY_INTERN管理这个Token,避免每次请求都重新登录。
第三种是OAuth 2.0的Client Credentials模式,需要先拿Client ID和Client Secret去换Access Token。这个稍微复杂,但原理跟第二种一样,只是换Token的接口和参数格式有固定套路。我用一个类CL_OAUTH_CLIENT来封装了一下,实测可用。
第四种是SSL双向认证,这种需要导入证书到SAP的证书库,然后在SM59目的地里配置SSL证书。ABAP代码里不需要做什么特殊处理,但证书过期是家常便饭,项目上务必做好监控。
不管哪种鉴权方式,我都有一个建议:测试用例里不要把真实密码或者Token硬编码进去。要么从配置表里读,要么通过PARAMETER传入。否则代码一传输,等于把凭据分发到全公司了,这是很要命的低级错误。
5. 常见问题与排查技巧实录
5.1 HTTP 400 Bad Request的排查方法
HTTP 400是ABAP调RESTful接口时最常遇到的问题,没有之一。对端返回400,说明你的请求报文有语法或语义问题,但对端又不告诉你具体是哪错了。
我的排查套路是三步走。第一步,把发送的原始报文完整打出来,用工具类的日志记录,别靠猜。第二步,去试同一个接口,用现成的接口测试工具(比如某个开源的API调试工具)发同样的报文,如果工具能通而ABAP不通,那就是报文在ABAP里被改动过,重点检查字符串拼接时的引号和中文转义。第三步,如果工具也报400,那就是报文内容本身不对,跟ABAP无关,去跟对端对字段定义。
我碰到过一个实际案例:某个字段在接口文档里是createTime,我按驼峰写的,结果对端要求是create_time,ABAP里字符串硬拼的话根本不报错,但对端直接400。这种问题查起来最费时间,因为错误信息基本没有参考价值。
5.2 超时与连接池耗尽问题
ABAP调用外部HTTP接口,默认的超时时间是60秒。这个时间在处理大量数据同步的场景下可能不够,尤其对端是大数据平台,你发个查询请求,它要跑几分钟才有结果。
超时的设置方式是在发送请求前调用:
lo_client->propertytype_accept_cookie = if_http_client=>co_enabled. lo_client->propertytype_logon_user = if_http_client=>co_unknown.严格来讲,超时一般是设置LO_CLIENT->PROPERTYTYPE_TIMEOUT,比如设置为300秒:
lo_client->propertytype_timeout = 300.但更常用的做法是,在SM59目的地的高级设置里把超时时间调大。如果某个接口很特殊,只在极少数场景下才需要长超时,可以在业务封装层单独创建一个专用目的地,不影响其他接口的默认配置。
连接池耗尽是另外一个容易被忽略的问题。SAP应用服务器到外部系统的HTTP连接数是有限制的,如果每次调用不CLOSE,连接就会挂在那儿,直到超时被回收,期间新的请求只能排队等。我遇到过最夸张的一次,Java服务那边正常,SAP这边所有HTTP调用全部超时,查到最后就是某个巡检程序忘了加CLOSE,连接池被占满了。
5.3 响应中文乱码的根治方案
ABAP里的Encoding问题非常阴间。多数外部系统返回的JSON都是UTF-8编码,但ABAP内部字符串默认可能是非Unicode环境的系统编码。如果你直接get_cdata,读出来的中文就是一堆问号或者乱码。
我的解决方案是,在接收响应后、解析Body前,做一次显式编码转换。具体做法是先把响应内容当成XSTRING读出来,然后按UTF-8转换到字符串。
DATA: lv_xstring TYPE xstring. DATA: lv_utf8 TYPE string. lv_xstring = lo_client->response->get_cdata( ). " 这里使用系统类转换编码 CALL METHOD cl_http_utility=>decode_xstring_to_utf8 EXPORTING encoded = lv_xstring RECEIVING decoded = lv_utf8.这个处理在工具类中做一次,所有测试用例都能受益。另外要注意的是,发送请求体的时候,如果Body里有中文,也要确认对端能接受UTF-8编码,否则就要在Header里设置Content-Type: text/json; charset=utf-8之类的内容。
5.4 JSON解析的常见坑与插件选择
ABAP原生没有JSON解析器,但系统提供了几个标准工具类。老一点的项目里用得比较多的是CL_TREX_JSON_SERIALIZER,新版本里有更顺手的CL_JSON接口。
这里有个很实用的经验:CL_TREX_JSON_SERIALIZER在处理JSON数组转内部表的时候,默认行为和很多人的预期不一样,经常出现结构对不上、数据丢失的问题。如果你用这个类解析总是报转换错误,别死磕,直接换CL_JSON试试,大概率能解决。
但不管用哪个类,JSON解析最大的坑是字段类型不匹配。比如JSON里某个字段是数字,ABAP内表里对应字段定义成CHAR,解析的时候就会直接报错。我建议在解析之前,先通过simple transformation或者XSLT做一个类型检查,或者干脆把内表字段全部定义为STRING,解析成功后再做类型转换。后一种方式虽然绕,但胜在稳定,适合接口字段频繁变动的项目。
6. SM59配置要点与多环境切换
6.1 SM59目的地创建的完整步骤
这套测试用例能不能跑起来,一半靠代码,一半靠SM59配置。配置不对,代码写得再漂亮也白搭。
进入事务码SM59,创建新的HTTP目的地,主要填几个地方:
- 目标主机地址:填外部系统的IP或域名;
- 服务端口号:比如8080、8443;
- 协议:选HTTP还是HTTPS;
- 路径前缀:如果外部接口统一都有个前缀,填这里,代码里只写相对路径;
- 代理设置:如果环境需要代理,这里配置,否则留空。
创建完成后,用SM59自带的连接测试功能先测一把。这一步能排除很多低级问题,比如IP不通、端口被防火墙挡了、证书不匹配等。千万别一上来就调代码,大概率是浪费时间。
6.2 测试环境与生产环境的无缝切换
项目上最烦的就是环境切换。测试环境的URL是test.api.example.com,生产是api.example.com,如果代码里写死地址,每次上线都要改代码、走传输,麻烦不说,还容易出现生产事故。
我的做法是,同一套代码,配置两个SM59目的地:一个叫ZEXTERNAL_API_TEST,一个叫ZEXTERNAL_API_PROD。测试用例默认连测试目的地,要验证生产时,临时改一下工具类里的目的地名称常量,加上一个写死的切换标记。或者在测试用例程序里增加一个参数,允许运行时选择目的地名称。
更彻底的做法是,目的地名不写死在代码里,而是放到一个配置表里,用部署环境做关键字去读。但大多数项目没那么复杂,做到“改一个变量就算切换”已经完全够用了。
6.3 证书与SSL的注意事项
如果是HTTPS接口,SM59里要指定SSL证书。SAP的证书管理在STRUST事务码里,需要把对端的CA证书导入进去。这个步骤容易出问题的地方是,证书链不完整,或者证书要求是PEM格式但你导入了DER格式。
实测下来,最简单的自测方法还是SM59里的连接测试。如果报SSL握手失败,八成是证书问题。这时候把对端提供的完整证书链(包含根证书和中间证书)全部导入,基本能解决。
另外提醒一下,很多测试环境的证书是自签的,SAP默认会拒绝。这时候需要在STRUST里把测试环境的证书设置为可信。我见过不止一个项目,代码写得好好的,结果在测试环境因为证书问题无法验证,白白浪费了好几天。
7. 使用体会与扩展建议
7.1 这套方案在真实项目中的表现
这套测试用例框架,我在一个电商集成的模拟项目中用了差不多三个月。期间接了四五个外部接口,包括订单查询、库存预留、物流状态回传、支付结果通知,有GET有POST,有同步有异步回调。整体跑下来,代码本身的改动很少,更多的是调SM59配置和排查对端接口的参数变化。
最让我满意的一点是,业务方排障的时间大幅缩短。以前接口出问题,第一反应是找ABAP开发看代码,现在直接把测试用例跑一遍,请求报文、响应报文、状态码全部打出来,是哪端的问题一目了然。有两次是外部系统那边改了字段名,导致我们发送的报文校验失败,测试用例的完整日志直接把问题暴露了,省去了好几轮“你查查那边”“那边没问题”的扯皮。
7.2 后续扩展的三个方向
如果这套基础框架已经跑通了,我建议再往三个方向扩展。
第一个方向是加自动化回归能力。用ABAP的定时任务调度测试用例,每天凌晨跑一遍核心接口联通性测试,输出日报,提前发现接口漂移或者证书过期的问题。
第二个方向是加响应时间监控。现在的测试用例只记录单次耗时,如果能加个历史存储表,按接口维度汇总平均耗时、最大耗时,就能在外部系统性能劣化时第一时间发现。
第三个方向是加JSON Schema校验。对端接口改字段时,如果测试用例能自动检查响应报文是否还符合约定的结构,就能避免“接口通但业务数据解析不出来”的隐性故障。
7.3 最后分享一个小技巧
调RESTful接口的时候,很多人忽略了一个细节:SAP的HTTP客户端默认可能会自动追加一些Header,或者对URL做一些处理。如果你发现对端收到的报文和代码里设置的不一致,可以用Wireshark或者直接在对端系统上加日志来对比。
另外,如果外部系统要求精确控制HTTP协议版本,比如强制用HTTP/1.1而不是HTTP/1.0,记得在SM59目的地的高级选项卡里设置协议版本。有些老的接口实现,在HTTP/1.0下行为会变,响应返回不完整,但ABAP端看起来只是数据少了一段,非常误导排查方向。
我自己在这上面吃过一次亏,整整调了一个下午,最后发现是协议版本的问题。测试用例本身没有报错,但响应里总是少了一个字段,后来比对网络抓包才发现对端返回是按不同协议走的。这种问题,光靠测试用例不够,必须结合网络层面的工具一起看。
所以我的建议是,SAP ABAP调RESTful API,不要只盯着代码,要把SM59配置、协议细节、对端接口文档这三样东西当成一个整体来对待。代码只是最后一步的执行者,前面任何一环出错,代码都会表现出各种稀奇古怪的症状。这套测试用例能做的,就是把症状清晰地呈现出来,帮你更快地找到病根。