RAP BO를 테스트하지 않으면 생기는 일 — 이 글의 목표
RAP(ABAP RESTful Application Programming Model)으로 Business Object를 만들다 보면 Validation, Determination, Action 로직이 Behavior Implementation 클래스 안에 빠르게 쌓입니다. 문제는 이 로직의 검증 수단이 Fiori 화면 수동 클릭뿐이라면, 필드 하나를 고칠 때마다 전체 시나리오를 다시 눌러봐야 한다는 점입니다. Validation 조건 하나가 바뀌면서 멀쩡하던 주문 생성이 전부 막히는 회귀(regression) 사고는 테스트 없는 RAP 프로젝트에서 가장 흔한 장애 유형입니다.
이 글에서 다루는 내용을 체크리스트로 정리하면 다음과 같습니다.
- RAP Behavior Implementation을 ABAP Unit으로 검증하는 전체 구조 이해
- CUT(Class Under Test) 개념과 RAP에서 EML 기반 테스트 진입점 설정
- OSQL/CDS Test Environment로 데이터베이스 의존성 격리
- BO Test Double Framework로 의존 BO 격리
- Given – When – Then 3단계 패턴으로 SalesOrder BO 테스트 작성
- 커버리지 측정과 ATC/CI 파이프라인 연계 흐름
시작 전에 갖춰야 할 배경
이 글은 중급 난이도를 전제로 합니다. CDS View Entity와 Behavior Definition의 기본 문법, EML(Entity Manipulation Language)의 MODIFY ENTITIES / READ ENTITIES 구문, ABAP OO 클래스 작성 경험이 있으면 무리 없이 따라올 수 있습니다. ABAP Unit 자체를 처음 접해도 괜찮습니다. 테스트 클래스 문법은 1단계 예제에서 처음부터 설명합니다.
환경, 버전, 준비물
아래 환경을 기준으로 작성했습니다.
- SAP BTP ABAP Environment 또는 SAP S/4HANA 2022 이상(ABAP Platform 2022 기준). Managed / Unmanaged 시나리오 모두 동일한 기법이 적용됩니다.
- ABAP Development Tools(ADT) 최신 버전 — 테스트 실행(Ctrl+Shift+F10)과 커버리지 측정(Ctrl+Shift+F11)은 ADT에서 수행합니다. SAP GUI로는 RAP 개발·테스트가 지원되지 않습니다.
- 예제 대상 BO: CDS Root View
ZR_SALESORDER, Behavior PoolZBP_R_SALESORDER, 저장 테이블ZSALESORDER_T, 의존 BOZR_CREDITLIMIT를 가정합니다. - Test Double 관련 클래스(
CL_OSQL_TEST_ENVIRONMENT,CL_CDS_TEST_ENVIRONMENT,CL_BOTD_TXBUFDBL_BO_TEST_ENV)는 별도 설치 없이 릴리스에 포함되어 있으며, ABAP Cloud 개발 모델에서도 릴리스된 API로 사용할 수 있습니다.
ABAP Unit과 RAP의 관계 — 핵심 개념
ABAP Unit은 ABAP 언어에 내장된 테스트 프레임워크입니다. FOR TESTING이 붙은 로컬/글로벌 클래스가 테스트 컨테이너가 되고, 그 안의 각 메서드가 독립적인 테스트 케이스가 됩니다. 테스트 코드는 프로덕션 코드와 함께 전송(transport)되지만 일반 런타임에는 로드되지 않으므로 운영 성능에 부담을 주지 않습니다.
RAP 테스트가 특별한 이유는 Behavior Implementation의 핸들러 메서드를 직접 호출할 수 없다는 점입니다. lhc_SalesOrder 같은 로컬 핸들러 클래스의 validateOrderDate 메서드는 RAP 런타임 프레임워크 전용 시그니처를 가지므로 테스트 코드에서 lo_handler->validateOrderDate( )처럼 부를 수 없습니다. 그래서 RAP 테스트의 진입점은 메서드 호출이 아니라 EML 구문입니다. 테스트에서 MODIFY ENTITIES로 데이터를 밀어 넣으면 RAP 런타임이 Determination과 Validation을 자동으로 트리거하고, 우리는 그 결과 구조(mapped, failed, reported)와 트랜잭션 버퍼 상태를 검증합니다.
비유하자면 엔진 부품(핸들러 메서드)을 분해해 하나씩 두드려 보는 방식이 아니라, 시동 키(EML)를 돌리고 계기판(failed/reported 응답)을 읽는 방식입니다. 이때 실제 도로(데이터베이스, 의존 BO)로 나가면 테스트가 느려지고 다른 테스트와 간섭하므로, 도로 대신 시뮬레이터(Test Double)를 깔아 주는 것이 핵심 전략입니다. 격리 대상 계층별로 쓰는 도구가 다릅니다.
| 격리 대상 | 프레임워크 | 대표 클래스 |
|---|---|---|
| DB 테이블 (ABAP SQL) | OSQL Test Environment | CL_OSQL_TEST_ENVIRONMENT |
| CDS View | CDS Test Double Framework | CL_CDS_TEST_ENVIRONMENT |
| 의존 RAP BO | BO Test Double Framework | CL_BOTD_TXBUFDBL_BO_TEST_ENV |
| 일반 클래스/인터페이스 | ABAP Test Double Framework | CL_ABAP_TESTDOUBLE |
여기서 CUT(Class Under Test)는 형식적으로는 Behavior Pool 클래스 ZBP_R_SALESORDER이지만, 실질적인 테스트 단위는 "EML을 통해 관찰되는 BO의 행동 전체"라고 이해하는 편이 정확합니다. 테스트 클래스는 Behavior Pool의 Test Classes 인클루드(ADT 하단 탭)에 작성하는 것이 일반적입니다.
실전 코드 1단계 — 기본 테스트 클래스와 첫 EML 테스트
ZBP_R_SALESORDER의 Test Classes 탭에 테스트 클래스를 만듭니다. 데이터베이스 격리 환경은 비용이 크므로 class_setup(클래스당 1회)에서 생성하고, 각 테스트 전마다 setup에서 버퍼를 비웁니다.
CLASS ltc_salesorder_bo DEFINITION FINAL
FOR TESTING
RISK LEVEL HARMLESS
DURATION SHORT.
PRIVATE SECTION.
CLASS-DATA sql_env TYPE REF TO if_osql_test_environment.
CLASS-METHODS class_setup.
CLASS-METHODS class_teardown.
METHODS setup.
METHODS teardown.
"1단계: 생성이 정상 동작하는지 확인
METHODS create_order_succeeds FOR TESTING RAISING cx_static_check.
ENDCLASS.
CLASS ltc_salesorder_bo IMPLEMENTATION.
METHOD class_setup.
"저장 테이블을 Test Double로 교체 → 실제 DB에 쓰지 않음
sql_env = cl_osql_test_environment=>create(
i_dependency_list = VALUE #( ( 'ZSALESORDER_T' ) ) ).
ENDMETHOD.
METHOD class_teardown.
sql_env->destroy( ).
ENDMETHOD.
METHOD setup.
sql_env->clear_doubles( ).
ENDMETHOD.
METHOD teardown.
ROLLBACK ENTITIES. "트랜잭션 버퍼 초기화
ENDMETHOD.
METHOD create_order_succeeds.
"When: EML로 주문 생성
MODIFY ENTITIES OF zr_salesorder
ENTITY SalesOrder
CREATE FIELDS ( CustomerId OrderDate NetAmount )
WITH VALUE #( ( %cid = 'ORD1'
CustomerId = 'C0001'
OrderDate = cl_abap_context_info=>get_system_date( )
NetAmount = '1500.00' ) )
MAPPED DATA(mapped)
FAILED DATA(failed)
REPORTED DATA(reported).
"Then: 실패 없이 키가 매핑되었는지 검증
cl_abap_unit_assert=>assert_initial( failed-salesorder ).
cl_abap_unit_assert=>assert_not_initial( mapped-salesorder ).
ENDMETHOD.
ENDCLASS.
포인트는 세 가지입니다. 첫째, RISK LEVEL HARMLESS / DURATION SHORT를 명시해 CI에서 항상 실행되는 안전한 테스트로 분류합니다. 둘째, Managed BO의 숫자 채번이 Late Numbering이라면 mapped에 임시 키(%pid 기반)가 담기므로 키 값 자체보다 "매핑 존재 여부"를 검증합니다. 셋째, teardown의 ROLLBACK ENTITIES가 테스트 간 버퍼 오염을 막는 안전핀입니다.
실전 코드 2단계 — Validation 실패 케이스와 reported 메시지 검증
실무에서 가치가 큰 테스트는 성공 경로보다 실패 경로입니다. 과거 날짜 주문을 거부하는 Validation validateOrderDate를 검증해 보겠습니다. Validation은 기본적으로 저장 시점(on save)에 트리거되므로 COMMIT ENTITIES까지 실행해야 결과가 나타납니다.
METHOD create_with_past_date_fails.
"Given: 과거 날짜 준비
DATA(past_date) = cl_abap_context_info=>get_system_date( ) - 30.
"When: 생성 후 커밋 → on save Validation 트리거
MODIFY ENTITIES OF zr_salesorder
ENTITY SalesOrder
CREATE FIELDS ( CustomerId OrderDate NetAmount )
WITH VALUE #( ( %cid = 'BAD1'
CustomerId = 'C0001'
OrderDate = past_date
NetAmount = '900.00' ) )
MAPPED DATA(mapped) FAILED DATA(failed) REPORTED DATA(reported).
COMMIT ENTITIES
RESPONSE OF zr_salesorder
FAILED DATA(commit_failed)
REPORTED DATA(commit_reported).
"Then: 커밋이 거부되고 메시지가 기록되어야 함
cl_abap_unit_assert=>assert_not_initial(
act = commit_failed-salesorder
msg = |과거 날짜 주문이 저장을 통과했습니다|
quit = if_abap_unit_constant=>quit-no ).
READ TABLE commit_reported-salesorder INTO DATA(msg_line) INDEX 1.
cl_abap_unit_assert=>assert_bound( msg_line-%msg ).
"어떤 필드에 대한 에러인지까지 검증하면 회귀 방지 효과가 큼
cl_abap_unit_assert=>assert_equals(
exp = if_abap_behv=>mk-on
act = msg_line-%element-orderdate ).
ENDMETHOD.
진단 편의를 위한 팁 두 가지. assert_*의 msg 파라미터에 실패 상황 설명을 넣으면 ADT 테스트 리포트에서 원인을 바로 읽을 수 있고, quit = quit-no로 지정하면 첫 실패 후에도 나머지 검증이 계속 실행되어 한 번의 실행으로 여러 결함을 관찰할 수 있습니다. 여러 케이스가 반복된다면 검증 로직을 private 헬퍼 메서드로 추출해 테스트 코드 자체의 중복도 관리하는 것이 일반적으로 권장됩니다.
실전 코드 3단계 — 의존 BO 격리와 프로덕션 수준 테스트
SalesOrder BO가 할인 Action에서 신용 한도 BO ZR_CREDITLIMIT를 EML로 읽는다면, 그 BO까지 실제로 실행하는 순간 단위 테스트가 통합 테스트로 변질됩니다. RAP BO Test Double Framework의 Transactional Buffer Double을 쓰면 의존 BO를 인메모리 버퍼로 대체하고, 테스트가 원하는 데이터를 그 버퍼에 직접 주입할 수 있습니다.
CLASS ltc_salesorder_isolated DEFINITION FINAL
FOR TESTING RISK LEVEL HARMLESS DURATION SHORT.
PRIVATE SECTION.
CLASS-DATA bo_env TYPE REF TO if_botd_txbufdbl_bo_test_env.
CLASS-DATA sql_env TYPE REF TO if_osql_test_environment.
CLASS-METHODS class_setup.
CLASS-METHODS class_teardown.
METHODS setup.
METHODS discount_blocked_over_limit FOR TESTING RAISING cx_static_check.
ENDCLASS.
CLASS ltc_salesorder_isolated IMPLEMENTATION.
METHOD class_setup.
"의존 BO를 트랜잭션 버퍼 Double로 교체
DATA(env_config) =
cl_botd_txbufdbl_bo_test_env=>prepare_environment_config(
)->set_bdef_dependencies(
bdef_dependencies = VALUE #( ( 'ZR_CREDITLIMIT' ) ) ).
bo_env = cl_botd_txbufdbl_bo_test_env=>create(
environment_config = env_config ).
sql_env = cl_osql_test_environment=>create(
i_dependency_list = VALUE #( ( 'ZSALESORDER_T' ) ) ).
ENDMETHOD.
METHOD class_teardown.
bo_env->destroy( ).
sql_env->destroy( ).
ENDMETHOD.
METHOD setup.
bo_env->clear_doubles( ).
sql_env->clear_doubles( ).
ENDMETHOD.
METHOD discount_blocked_over_limit.
"=== Given: Double 버퍼에 신용한도 데이터 주입 ===
MODIFY ENTITIES OF zr_creditlimit
ENTITY CreditLimit
CREATE FIELDS ( CustomerId LimitAmount )
WITH VALUE #( ( %cid = 'CL1'
CustomerId = 'C0001'
LimitAmount = '1000.00' ) )
MAPPED DATA(m1) FAILED DATA(f1) REPORTED DATA(r1).
cl_abap_unit_assert=>assert_initial( f1 ).
"=== When: 한도 초과 주문에 할인 Action 실행 ===
MODIFY ENTITIES OF zr_salesorder
ENTITY SalesOrder
CREATE FIELDS ( CustomerId NetAmount )
WITH VALUE #( ( %cid = 'ORD1'
CustomerId = 'C0001'
NetAmount = '5000.00' ) )
ENTITY SalesOrder
EXECUTE applyDiscount FROM VALUE #( ( %cid_ref = 'ORD1' ) )
MAPPED DATA(mapped) FAILED DATA(failed) REPORTED DATA(reported).
"=== Then: Action이 거부되어야 함 ===
cl_abap_unit_assert=>assert_not_initial(
act = failed-salesorder
msg = |신용한도 초과인데 할인이 적용되었습니다| ).
ENDMETHOD.
ENDCLASS.
프로덕션 관점 보강 사항입니다. (1) 성능: 환경 생성은 class_setup에 두고 데이터 주입만 테스트마다 수행하면 수백 개 테스트도 수 초 내에 끝납니다. (2) 보안: Test Double은 실제 테이블에 쓰지 않으므로 QA 데이터 오염 위험이 없고, HARMLESS 등급을 유지하면 운영성 시스템에서도 안전하게 실행됩니다. (3) 응답 제어가 필요한 경우: 의존 BO가 특정 오류를 반환하는 시나리오를 강제하고 싶다면 버퍼 Double 대신 CL_BOTD_MOCKEMLAPI_BO_TEST_ENV(Mock EML API)로 EML 요청·응답을 직접 구성하는 방식이 적합합니다.
3단계 패턴 요약 — Given, When, Then
지금까지의 흐름을 재사용 가능한 패턴으로 정리하면 다음 세 단계입니다.
- Given(격리·준비) —
class_setup에서 OSQL/CDS/BO Double 환경 생성. 읽기 전제 데이터는sql_env->insert_test_data( )또는 Double 버퍼에 대한 EML CREATE로 주입. - When(실행) — 검증 대상 행동을 EML 한 번으로 트리거. Determination
on modify는 MODIFY 직후READ ENTITIES로, Validationon save는COMMIT ENTITIES후 응답으로 관찰. - Then(검증) —
mapped/failed/reported구조와 버퍼 상태를cl_abap_unit_assert로 확인. 메시지의%element마킹까지 검증하면 회귀 감지력이 크게 올라갑니다.
테스트 메서드 이름을 행동_조건_기대결과(예: create_with_past_date_fails) 형태로 짓고, 한 메서드에 하나의 시나리오만 담는 것이 유지보수에 유리합니다.
흔한 실수와 트러블슈팅 FAQ
Q1. Validation 테스트인데 failed가 항상 비어 있습니다. — Validation은 대부분 on save 트리거입니다. MODIFY ENTITIES만으로는 아직 실행되지 않은 상태이므로, 반드시 COMMIT ENTITIES ... RESPONSE OF를 호출하고 커밋 응답의 failed를 검증해야 합니다. 반대로 Determination on modify는 커밋 없이 READ로 확인 가능합니다.
Q2. 테스트를 두 번 이상 실행하면 두 번째부터 실패합니다. — 트랜잭션 버퍼나 Double 데이터 잔재가 원인인 경우가 대부분입니다. setup의 clear_doubles( ), teardown의 ROLLBACK ENTITIES를 빠뜨리지 않았는지 확인하세요. 같은 %cid를 재사용하는 것도 동일한 증상을 만듭니다.
Q3. lhc 클래스의 메서드를 직접 호출해 테스트하고 싶습니다. — 핸들러 메서드는 RAP 런타임 전용이라 직접 호출이 불가능하고, 시도 자체가 안티패턴입니다. 계산 로직이 복잡하다면 순수 헬퍼 클래스로 추출해 CL_ABAP_TESTDOUBLE 기반 일반 단위 테스트로 검증하고, BO 행동은 EML 테스트로 나누는 이중 구성이 권장됩니다.
Q4. Managed BO에서 OSQL Double과 CDS Double 중 무엇을 써야 하나요? — 쓰기 경로(저장 테이블)는 CL_OSQL_TEST_ENVIRONMENT, 조인·계산 필드가 많은 읽기 경로(CDS View)는 CL_CDS_TEST_ENVIRONMENT가 적합합니다. 한 테스트 클래스에서 두 환경을 함께 생성해도 문제없습니다.
커버리지 확인과 CI/CD 통합, 이어서 볼 주제
ADT에서 Ctrl+Shift+F11(커버리지 포함 실행)을 누르면 Statement/Branch 커버리지가 에디터에 색상으로 표시되어, Validation의 분기 중 어느 쪽이 검증되지 않았는지 즉시 보입니다. 조직 차원에서는 ATC(ABAP Test Cockpit) 검사 변형에 ABAP Unit 실행을 포함시켜 전송 릴리스 시점에 테스트를 강제하고, BTP ABAP Environment라면 gCTS와 Project "Piper"의 ABAP Environment Pipeline에 단위 테스트 스테이지를 연결하는 방식이 일반적입니다. 이 글 다음으로는 Mock EML API를 활용한 오류 응답 시나리오 제어, Draft BO의 Additional Save/Draft Action 테스트, OData 레벨 통합 테스트(Service Consumption 관점)를 이어서 살펴보면 RAP 테스트 피라미드를 완성할 수 있습니다.
더 읽어볼 문서
댓글 0
아직 댓글이 없습니다.