이 글에서 다루는 내용과 확인 체크리스트
RAP(RESTful ABAP Programming Model)로 Business Object를 개발할 때 가장 흔하게 놓치는 부분이 바로 Behavior Implementation Class에 대한 단위 테스트입니다. UI만 눈으로 확인하고 넘어가면 검증되지 않은 로직이 프로덕션까지 흘러가고, 결국 통합 테스트 단계에서 원인을 찾느라 며칠을 소비하게 됩니다. 이 글은 ABAP Test Double Framework를 활용해 실제 DB 접근 없이 RAP BO의 동작을 안정적으로 검증하는 3단계 패턴을 다룹니다.
- RAP BO Behavior Implementation을 테스트해야 하는 이유 이해
- ABAP Unit Test 3단계(Given / When / Then) 구조 습득
- ABAP Test Double Framework(td)로 EML 대체 방법 학습
- SalesOrder 시나리오 기반 실전 코드 작성
- cl_abap_unit_assert로 결과 검증 및 CI 파이프라인 연동
이 글을 읽기 전에 갖추면 좋은 배경
ABAP OO(클래스, 인터페이스, 메서드 재정의) 기본 문법과 RAP의 Managed / Unmanaged Behavior 개념을 대략적으로 이해하고 있어야 합니다. 또한 EML(Entity Manipulation Language)의 MODIFY ENTITIES, READ ENTITIES 구문이 어떤 상황에서 호출되는지 감각적으로 알고 있으면 코드 예제를 따라가기 편합니다. ADT(ABAP Development Tools) 사용 경험과 ABAP Unit Runner(Ctrl+Shift+F10)를 실행해 본 적이 있다면 곧바로 실습에 들어갈 수 있습니다.
실습 환경과 준비 사항
이 글의 코드는 SAP BTP ABAP Environment(Steampunk) 2308 릴리즈 이상 또는 S/4HANA 2022 온프레미스에서 검증되었습니다. RAP는 릴리즈별로 사용 가능한 기능(예: Draft, Determination, Action)이 다르므로 문서와 시스템 버전을 반드시 매칭해서 확인하는 것이 일반적으로 권장됩니다.
- ABAP Development Tools(ADT) for Eclipse 2024-03 이상
- ABAP Language Version:
ABAP for Cloud Development또는Standard ABAP - ABAP Unit 실행 권한(
S_DEVELOP의 ACTVT 16) - 테스트 대상 RAP BO 하나 이상(예:
ZI_SalesOrder) 준비 - ABAP Test Double Framework(
if_abap_testdouble) 사용 가능한 커널
핵심 개념: 왜 RAP BO는 테스트가 어려운가
RAP BO의 Behavior Implementation Class는 프레임워크가 특정 시점에 콜백처럼 호출하는 구조입니다. 일반 클래스처럼 NEW로 인스턴스를 만들어 호출할 수 없고, EML을 통해서만 트리거됩니다. 이 지점에서 개발자가 흔히 만나는 벽이 두 가지 있습니다.
첫째, Behavior Implementation의 FOR MODIFY, FOR VALIDATE ON SAVE, FOR DETERMINE 같은 메서드는 매개변수 구조가 프레임워크에 종속됩니다. keys, entities, failed, reported 같은 파라미터를 임의로 채워야 하는데, 잘못 채우면 실제 런타임과 다른 결과가 나옵니다.
둘째, RAP BO는 내부적으로 다른 BO를 조회하거나 저장하기 위해 EML을 재호출합니다. 이 부분을 실제 DB로 흘려보내면 테스트가 느려지고, 트랜잭션 롤백 처리가 꼬입니다. 그래서 ABAP Test Double Framework(td)가 등장합니다. 아래는 개념 도식입니다.
실제 런타임: [Test Class] → [EML MODIFY] → [BO Behavior Pool] → [DB]
테스트: [Test Class] → [td가 가로챈 EML] → [Behavior Pool CUT] → [In-Memory 응답]
td는 마치 무대 뒤에서 대사를 대신 읽어주는 대역 배우(Test Double)와 같습니다. 실제 데이터베이스 없이도 "MODIFY ENTITY가 호출되면 이런 결과를 돌려줘라"라고 스크립트를 짜두고, CUT(Class Under Test)만 실제로 실행합니다. 이렇게 하면 로직 검증에만 집중할 수 있고, 테스트 실행 시간도 수십 밀리초 안에 끝납니다.
추가로 알아둘 개념이 CUT(Class Under Test)입니다. 관습적으로 테스트 클래스 안에서 DATA cut TYPE REF TO ... 형태로 선언해 두면, 이후 코드가 "지금 검증 대상이 뭔지" 한눈에 들어옵니다. RAP Behavior Implementation은 로컬 클래스(lhc_salesorder)이므로 friend 관계를 걸어 접근해야 합니다.
실전 코드 1단계: 기본 골격 만들기
먼저 가장 단순한 형태의 테스트 클래스를 만들어 봅니다. 시나리오는 ZI_SalesOrder BO의 calculateTotalAmount라는 Determination을 검증하는 것입니다. 이 Determination은 헤더의 Quantity와 UnitPrice를 곱해 TotalAmount를 채워 넣는 로직을 담고 있다고 가정합니다.
CLASS ltcl_salesorder DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA cut TYPE REF TO lhc_salesorder.
METHODS:
setup,
calculate_total_basic FOR TESTING RAISING cx_static_check.
ENDCLASS.
CLASS ltcl_salesorder IMPLEMENTATION.
METHOD setup.
CREATE OBJECT cut FOR TESTING.
ENDMETHOD.
METHOD calculate_total_basic.
" Given: 수량 5, 단가 200 인 헤더
DATA(keys) = VALUE zi_salesorder_key_tab(
( %key-SalesOrderId = '1000' ) ).
" When: Determination 호출
cut->calculateTotalAmount(
EXPORTING keys = keys ).
" Then: 별도 검증 없이 예외만 없으면 통과
cl_abap_unit_assert=>assert_bound( cut ).
ENDMETHOD.
ENDCLASS.
여기서 눈여겨볼 부분은 FOR TESTING, DURATION SHORT, RISK LEVEL HARMLESS 세 가지 애드온입니다. Duration은 예상 실행 시간, Risk Level은 이 테스트가 시스템에 어떤 영향을 주는지를 선언합니다. RAP 테스트는 원칙적으로 DB를 건드리지 않아야 하므로 HARMLESS가 표준입니다. lhc_salesorder는 Behavior Implementation의 로컬 클래스이므로, 이 테스트 클래스는 반드시 같은 include(로컬 타입) 안에 두거나 local friends로 지정되어야 합니다.
실전 코드 2단계: Test Double과 에러 시나리오
2단계에서는 실제로 EML을 대체하고, 실패(failed) 및 리포트(reported) 테이블을 검증합니다. 시나리오는 "SalesOrder를 저장하기 전에 CustomerId가 비어 있으면 실패로 처리한다"는 Validation입니다.
CLASS ltcl_salesorder_validate DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA:
cut TYPE REF TO lhc_salesorder,
sql_environment TYPE REF TO if_osql_test_environment.
CLASS-METHODS class_setup.
CLASS-METHODS class_teardown.
METHODS:
setup,
teardown,
validate_missing_customer FOR TESTING RAISING cx_static_check.
ENDCLASS.
CLASS ltcl_salesorder_validate IMPLEMENTATION.
METHOD class_setup.
" 실제 DB 테이블을 In-Memory Double 로 치환
sql_environment = cl_osql_test_environment=>create(
i_dependency_list = VALUE #( ( 'ZSALESORDER' ) ) ).
ENDMETHOD.
METHOD class_teardown.
sql_environment->destroy( ).
ENDMETHOD.
METHOD setup.
CREATE OBJECT cut FOR TESTING.
sql_environment->clear_doubles( ).
ENDMETHOD.
METHOD teardown.
ROLLBACK ENTITIES. " RAP 트랜잭션 버퍼 초기화
ENDMETHOD.
METHOD validate_missing_customer.
" Given: CustomerId 가 비어있는 SalesOrder 하나
DATA(test_data) = VALUE ztsalesorder_tab(
( sales_order_id = '2001' customer_id = '' amount = 100 ) ).
sql_environment->insert_test_data( test_data ).
DATA(keys) = VALUE zi_salesorder_key_tab(
( %key-SalesOrderId = '2001' ) ).
DATA:
failed TYPE RESPONSE FOR FAILED LATE zi_salesorder,
reported TYPE RESPONSE FOR REPORTED LATE zi_salesorder.
" When
cut->validateCustomer(
EXPORTING keys = keys
CHANGING failed = failed
reported = reported ).
" Then
cl_abap_unit_assert=>assert_not_initial(
act = failed-salesorder
msg = 'CustomerId 누락은 failed 로 리포트되어야 함' ).
cl_abap_unit_assert=>assert_equals(
exp = 'CUSTOMER_REQUIRED'
act = reported-salesorder[ 1 ]-%msg->if_t100_message~t100key-msgid
msg = '메시지 클래스가 CUSTOMER_REQUIRED 여야 함' ).
ENDMETHOD.
ENDCLASS.
여기서 등장한 cl_osql_test_environment는 실제 DB 테이블을 임시로 대체하여, SELECT 문이 insert_test_data로 넣어둔 데이터에서 조회되도록 만듭니다. 결과적으로 ROLLBACK ENTITIES만 잘 챙기면 DB 상태를 오염시키지 않고 여러 테스트를 반복 실행할 수 있습니다. Given / When / Then 3단 구조를 주석으로 명시하면 리뷰어가 흐름을 훨씬 빠르게 이해할 수 있습니다.
실전 코드 3단계: 프로덕션 수준의 EML 테스트
마지막 단계는 실제 EML(MODIFY ENTITIES)이 개입하는 Action을 테스트합니다. 시나리오는 releaseSalesOrder Action이 성공하면 상태가 R로 바뀌고, 이미 릴리즈된 주문이면 failed에 담긴다는 것입니다.
CLASS ltcl_salesorder_action DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA cut TYPE REF TO lhc_salesorder.
CLASS-DATA sql_env TYPE REF TO if_osql_test_environment.
CLASS-METHODS class_setup.
CLASS-METHODS class_teardown.
METHODS:
setup,
teardown,
release_success FOR TESTING RAISING cx_static_check,
release_already_released FOR TESTING RAISING cx_static_check.
ENDCLASS.
CLASS ltcl_salesorder_action IMPLEMENTATION.
METHOD class_setup.
sql_env = cl_osql_test_environment=>create(
i_dependency_list = VALUE #( ( 'ZSALESORDER' ) ) ).
ENDMETHOD.
METHOD class_teardown.
sql_env->destroy( ).
ENDMETHOD.
METHOD setup.
CREATE OBJECT cut FOR TESTING.
sql_env->clear_doubles( ).
ENDMETHOD.
METHOD teardown.
ROLLBACK ENTITIES.
ENDMETHOD.
METHOD release_success.
" Given: 상태가 'N'(New) 인 주문
sql_env->insert_test_data( VALUE ztsalesorder_tab(
( sales_order_id = '3001' status = 'N' amount = 500 ) ) ).
DATA:
keys TYPE TABLE FOR ACTION IMPORT zi_salesorder~releaseSalesOrder,
result TYPE TABLE FOR ACTION RESULT zi_salesorder~releaseSalesOrder,
failed TYPE RESPONSE FOR FAILED LATE zi_salesorder,
reported TYPE RESPONSE FOR REPORTED LATE zi_salesorder.
keys = VALUE #( ( %key-SalesOrderId = '3001' ) ).
" When
cut->releaseSalesOrder(
EXPORTING keys = keys
CHANGING result = result
failed = failed
reported = reported ).
" Then
cl_abap_unit_assert=>assert_initial(
act = failed-salesorder
msg = '정상 릴리즈는 failed 가 비어 있어야 함' ).
cl_abap_unit_assert=>assert_equals(
exp = 'R'
act = result[ 1 ]-%param-Status
msg = 'Action 결과 상태는 R 이어야 함' ).
ENDMETHOD.
METHOD release_already_released.
sql_env->insert_test_data( VALUE ztsalesorder_tab(
( sales_order_id = '3002' status = 'R' amount = 700 ) ) ).
DATA:
keys TYPE TABLE FOR ACTION IMPORT zi_salesorder~releaseSalesOrder,
result TYPE TABLE FOR ACTION RESULT zi_salesorder~releaseSalesOrder,
failed TYPE RESPONSE FOR FAILED LATE zi_salesorder,
reported TYPE RESPONSE FOR REPORTED LATE zi_salesorder.
keys = VALUE #( ( %key-SalesOrderId = '3002' ) ).
cut->releaseSalesOrder(
EXPORTING keys = keys
CHANGING result = result
failed = failed
reported = reported ).
cl_abap_unit_assert=>assert_not_initial(
act = failed-salesorder
msg = '중복 릴리즈는 failed 로 처리되어야 함' ).
ENDMETHOD.
ENDCLASS.
이 단계에서 중요한 포인트는 세 가지입니다. 첫째, TYPE TABLE FOR ACTION IMPORT와 ACTION RESULT는 RAP가 자동 생성하는 형식이므로 실수로 임의 구조를 만들지 말아야 합니다. 둘째, 동일한 CUT 인스턴스라도 setup에서 매번 새로 만들어야 이전 테스트의 상태가 새 나가지 않습니다. 셋째, 성공/실패 두 케이스를 반드시 쌍으로 작성해야 회귀 방지 효과가 생깁니다. CI 파이프라인(예: gCTS + abapGit + Jenkins)에서 ABAP Unit Test Runner를 실행할 때 이 두 케이스가 모두 통과해야만 트랜스포트가 승인되도록 게이트를 걸어두는 것을 권장합니다.
자주 겪는 실수와 해결 팁
Q1. lhc_salesorder가 인식되지 않습니다.
Behavior Implementation의 로컬 클래스는 기본적으로 외부에서 접근할 수 없습니다. 테스트 클래스를 같은 로컬 타입 include 안에 두거나, CLASS lhc_salesorder DEFINITION LOCAL FRIENDS ltcl_salesorder처럼 friend 관계를 명시해야 합니다.
Q2. ROLLBACK ENTITIES를 빼먹었더니 뒤 테스트가 이상하게 동작합니다.
RAP는 트랜잭션 버퍼(변경 사항 스테이징 영역)를 사용합니다. teardown에서 ROLLBACK ENTITIES를 호출하지 않으면 이전 테스트가 남긴 상태가 다음 테스트로 새어 나갑니다. 각 테스트가 독립적으로 실행되도록 반드시 정리하는 것이 안전합니다.
Q3. td가 SELECT는 잘 대체하는데 MODIFY ENTITIES는 대체가 안 됩니다.
cl_osql_test_environment는 DB 레벨의 SQL만 가로챕니다. EML 자체를 스텁하려면 cl_abap_behv_test_environment(BTP ABAP Environment)나 관련 헬퍼 클래스를 사용해야 하며, 릴리즈에 따라 지원 범위가 다르므로 시스템 문서를 먼저 확인하는 것이 일반적입니다. 지원되지 않는 환경에서는 Behavior 구현 안에서 EML을 별도 메서드로 분리해 두고 그 메서드만 단위 테스트하는 방식을 대안으로 씁니다.
Q4. 테스트는 통과하는데 프로덕션에서만 실패합니다.
대개 %cid(Content ID) 또는 %pky(Primary Key) 처리 누락입니다. 테스트에서 키를 하드코딩하면 실제 사용 시 %cid_ref로 들어오는 케이스를 놓칠 수 있습니다. 테스트 케이스에 %cid가 포함된 시나리오도 반드시 하나 추가하는 것을 권장합니다.
이 다음에 확장하면 좋은 주제
이 글에서 다룬 3단계 패턴을 익혔다면, 다음으로는 cl_abap_behv_aux를 활용한 Draft 시나리오 테스트, RAP Query Implementation의 read 메서드 테스트, 그리고 Service Binding까지 아우르는 Integration Test(Wrapper Test)로 확장하는 것을 권장합니다. 또한 gCTS + Jenkins 파이프라인에서 abap_ci_cli로 유닛 테스트를 자동 실행하도록 구성하면, 트랜스포트 승인 전에 회귀를 잡아낼 수 있습니다. ATC(ABAP Test Cockpit) 규칙에 "Behavior Implementation 클래스는 반드시 대응하는 test class를 가진다"는 커스텀 체크를 추가하면 팀 전체 품질이 균질해집니다.
참고할 만한 공식 문서와 커뮤니티 자료
댓글 0
아직 댓글이 없습니다.