이 글에서 다룰 것
학습 후 다음 항목을 수행할 수 있습니다.
- Mock / Stub / Spy의 차이를 ABAP 관점에서 설명할 수 있다.
- SQL 테스트 환경을 설정하고 Test Double 데이터를 주입할 수 있다.
- 인터페이스 기반 의존성 주입(Inject) 패턴으로 테스트 가능한 코드를 설계할 수 있다.
- 실제 항공편 조회 시나리오에 대해 결정적 단위테스트를 작성할 수 있다.
- Mock이 동작하지 않을 때 트러블슈팅을 진행할 수 있다.
이 글을 보기 전에
이 글은 advanced 난이도이며, 다음 내용을 이미 학습했다고 가정합니다.
- ABAP OO 기본 (인터페이스, 클래스, 생성자)
- ABAP Unit 기본 사용법 (
FOR TESTING,cl_abap_unit_assert) - Open SQL과 CDS View 기초
- Eclipse ADT(ABAP Development Tools) 사용 경험
테스트 환경
다음 환경에서 검증된 내용을 기반으로 합니다. 시스템 버전이 낮은 경우 일부 기능이 제한될 수 있습니다.
- SAP NetWeaver AS ABAP 7.51 이상 (CDS Test Double은 7.52 이상 권장)
- S/4HANA 1909 이상 또는 ABAP Cloud (Steampunk) 환경
- Eclipse + ABAP Development Tools 3.30 이상
- 테스트 패키지 권한:
S_DEVELOP, ABAP Unit 실행 권한 - 샘플 데이터: SAP 표준 비행 모델 (SFLIGHT/SCARR) 또는 ABAP Flight Reference Scenario
온프레미스의 경우 Note 2270890 등 ABAP Unit 관련 최신 노트를 적용해 두는 것이 일반적으로 권장됩니다.
왜 Test Double인가?
전통적인 ABAP 코드는 SELECT ... FROM sflight처럼 비즈니스 로직과 SQL이 한 메서드 안에 강하게 결합되어 있습니다. 이 경우 다음 문제가 발생합니다.
- 비결정성: 실제 DB 데이터가 바뀌면 테스트 결과가 달라집니다.
- 속도: 모든 테스트가 DB 라운드트립을 일으켜 느려집니다.
- 격리 부재: 다른 테스트가 만든 데이터가 영향을 줍니다.
- 전제 데이터 준비 비용: 외래키/필수 필드 충족을 위해 거대한 픽스처가 필요합니다.
Mock / Stub / Spy 개념 정리
Gerard Meszaros의 xUnit Test Patterns 분류를 ABAP 관점에서 이해하면 다음과 같습니다.
- Stub: "이 메서드를 호출하면 이 값을 반환해라" — 상태 기반 테스트의 입력 공급용.
cl_abap_testdouble의configure_call이 대표 예입니다. - Mock: "이 메서드가 정확히 이 인자로 N번 호출되어야 한다" — 행위 기반 검증.
verify_expectations가 이에 해당합니다. - Spy: 실제 호출은 그대로 두고 "몇 번 호출됐는지" 등 메타정보만 기록.
비유하자면 Stub은 "더미 점원이 정해진 답만 읽어주는 것", Mock은 "감독관이 점원의 응대 횟수와 멘트를 채점하는 것", Spy는 "CCTV로 횟수만 세는 것"입니다. ABAP의 CL_ABAP_TESTDOUBLE은 이 셋을 모두 한 객체에서 모드 전환으로 지원합니다.
직접 해보기
1. SQL Mock — CL_OSQL_TEST_ENVIRONMENT
이 클래스는 SQL 문이 실제 DB 테이블이 아닌 메모리상의 임시 테이블 또는 내부 가상 테이블(Test Double Table)을 바라보도록 OSQL 런타임을 가로챕니다. 즉, 코드 변경 없이 SELECT가 가짜 데이터를 읽도록 만들 수 있어 레거시 코드에도 적용 가능한 것이 큰 장점입니다.
SFLIGHT를 조회하는 단순 메서드를 테스트해 봅니다. 실제 DB가 비어 있어도 테스트는 통과해야 합니다.
CLASS zcl_flight_reader DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS read_by_carrier
IMPORTING iv_carrid TYPE sflight-carrid
RETURNING VALUE(rt_sflight) TYPE STANDARD TABLE OF sflight.
ENDCLASS.
CLASS zcl_flight_reader IMPLEMENTATION.
METHOD read_by_carrier.
SELECT * FROM sflight
WHERE carrid = @iv_carrid
INTO TABLE @rt_sflight.
ENDMETHOD.
ENDCLASS.
CLASS ltc_flight_reader DEFINITION FINAL FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
CLASS-DATA: go_env TYPE REF TO if_osql_test_environment.
DATA: mo_cut TYPE REF TO zcl_flight_reader.
CLASS-METHODS: class_setup, class_teardown.
METHODS: setup, when_select_then_return FOR TESTING.
ENDCLASS.
CLASS ltc_flight_reader IMPLEMENTATION.
METHOD class_setup.
go_env = cl_osql_test_environment=>create(
i_dependency_list = VALUE #( ( 'SFLIGHT' ) ) ).
ENDMETHOD.
METHOD class_teardown.
go_env->destroy( ).
ENDMETHOD.
METHOD setup.
go_env->clear_doubles( ).
mo_cut = NEW zcl_flight_reader( ).
ENDMETHOD.
METHOD when_select_then_return.
DATA: lt_double TYPE STANDARD TABLE OF sflight.
lt_double = VALUE #(
( carrid = 'LH' connid = '0400' fldate = '20260101' )
( carrid = 'LH' connid = '0401' fldate = '20260102' )
( carrid = 'AA' connid = '0017' fldate = '20260101' ) ).
go_env->insert_test_data( lt_double ).
DATA(lt_result) = mo_cut->read_by_carrier( 'LH' ).
cl_abap_unit_assert=>assert_equals(
exp = 2 act = lines( lt_result ) ).
ENDMETHOD.
ENDCLASS.
핵심은 create( i_dependency_list = ... )로 가짜로 만들 테이블 목록을 선언하고, 각 테스트마다 insert_test_data로 시나리오 데이터를 넣는다는 점입니다.
2. Test Double 주입 — CL_ABAP_TESTDOUBLE
실무에서는 SQL 자체가 아니라 "DAO 인터페이스"를 만들고 그 인터페이스를 mock 하는 패턴이 더 깔끔합니다. 비즈니스 로직과 영속성을 분리해 두면 단위테스트가 압도적으로 쉬워집니다.
INTERFACE zif_flight_dao.
METHODS read_by_carrier
IMPORTING iv_carrid TYPE sflight-carrid
RETURNING VALUE(rt_sflight) TYPE STANDARD TABLE OF sflight
RAISING zcx_flight_not_found.
ENDINTERFACE.
CLASS zcl_flight_service DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS constructor IMPORTING io_dao TYPE REF TO zif_flight_dao.
METHODS get_total_seats
IMPORTING iv_carrid TYPE sflight-carrid
RETURNING VALUE(rv_total) TYPE i.
PRIVATE SECTION.
DATA mo_dao TYPE REF TO zif_flight_dao.
ENDCLASS.
CLASS zcl_flight_service IMPLEMENTATION.
METHOD constructor.
mo_dao = io_dao.
ENDMETHOD.
METHOD get_total_seats.
TRY.
DATA(lt_flights) = mo_dao->read_by_carrier( iv_carrid ).
rv_total = REDUCE i( INIT s = 0
FOR <f> IN lt_flights
NEXT s = s + <f>-seatsmax ).
CATCH zcx_flight_not_found.
rv_total = 0.
ENDTRY.
ENDMETHOD.
ENDCLASS.
이제 테스트는 cl_abap_testdouble로 DAO 인터페이스를 통째로 가짜로 교체합니다.
CLASS ltc_flight_service DEFINITION FINAL FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA: mo_dao_dbl TYPE REF TO zif_flight_dao,
mo_cut TYPE REF TO zcl_flight_service.
METHODS:
setup,
sum_seats_when_two_flights FOR TESTING,
return_zero_when_not_found FOR TESTING.
ENDCLASS.
CLASS ltc_flight_service IMPLEMENTATION.
METHOD setup.
mo_dao_dbl ?= cl_abap_testdouble=>create( 'zif_flight_dao' ).
mo_cut = NEW zcl_flight_service( mo_dao_dbl ).
ENDMETHOD.
METHOD sum_seats_when_two_flights.
DATA lt_fake TYPE STANDARD TABLE OF sflight.
lt_fake = VALUE #( ( carrid = 'LH' seatsmax = 200 )
( carrid = 'LH' seatsmax = 150 ) ).
cl_abap_testdouble=>configure_call( mo_dao_dbl
)->returning( lt_fake ).
mo_dao_dbl->read_by_carrier( '' ). " 호출 패턴 기록
DATA(lv_total) = mo_cut->get_total_seats( 'LH' ).
cl_abap_unit_assert=>assert_equals( exp = 350 act = lv_total ).
ENDMETHOD.
METHOD return_zero_when_not_found.
DATA lo_ex TYPE REF TO zcx_flight_not_found.
lo_ex = NEW zcx_flight_not_found( ).
cl_abap_testdouble=>configure_call( mo_dao_dbl
)->raise_exception( lo_ex ).
mo_dao_dbl->read_by_carrier( '' ).
cl_abap_unit_assert=>assert_equals(
exp = 0
act = mo_cut->get_total_seats( 'XX' ) ).
ENDMETHOD.
ENDCLASS.
configure_call(...)->returning(...) 다음에 가짜 인터페이스 메서드를 한 번 호출해 "호출 패턴"을 기록하는 점이 ABAP Test Double의 독특한 사용 규칙입니다.
3. 행위 검증 — verify_expectations
운영 코드에서는 단순 반환값뿐 아니라 "정확히 이 carrid로 1번 호출됐는가?"를 검증해야 할 때가 있습니다.
METHOD verify_dao_called_exactly_once.
DATA lt_fake TYPE STANDARD TABLE OF sflight.
lt_fake = VALUE #( ( carrid = 'LH' seatsmax = 100 ) ).
cl_abap_testdouble=>configure_call( mo_dao_dbl
)->ignore_all_parameters(
)->returning( lt_fake ).
mo_dao_dbl->read_by_carrier( '' ).
mo_cut->get_total_seats( 'LH' ).
" 행위(Mock) 검증
cl_abap_testdouble=>verify_expectations( mo_dao_dbl ).
cl_abap_testdouble=>configure_call( mo_dao_dbl
)->times( 1 ).
mo_dao_dbl->read_by_carrier( 'LH' ).
ENDMETHOD.
삽질 노트
보안/성능 측면 팁은 다음과 같습니다.
- RISK LEVEL HARMLESS, DURATION SHORT를 명시해 운영 시스템에서도 안전하게 실행되도록 합니다.
- 대량 데이터 시뮬레이션이 필요할 때만
DURATION MEDIUM으로 올립니다. - 테스트 데이터에 실제 운영 데이터를 복사해 넣지 말고 합성 데이터를 사용합니다 (개인정보 노출 위험).
class_teardown에서destroy()를 잊으면 다음 테스트 클래스에 잔존 환경이 영향을 줄 수 있습니다.
자주 만나는 함정
configure_call( double )->returning( ... )만 호출하고 끝내면 아무 효과가 없습니다. 반드시 그 직후에 double->target_method( ... )를 호출해 "이런 호출이 오면 이렇게 응답하라"는 패턴을 기록해야 합니다. 이 단계가 ABAP Test Double 입문자가 가장 자주 빼먹는 부분입니다.
SQL Mock이 안 될 때
대표적 원인 3가지입니다.
- 해당 테이블이
cl_osql_test_environment=>create의i_dependency_list에 포함되지 않았다. NATIVE SQL/EXEC SQL/ADBC를 사용했다 — OSQL 런타임을 거치지 않으므로 가로챌 수 없습니다.- 테스트 클래스가
RISK LEVEL CRITICAL이거나DURATION LONG으로 실제 DB 변경을 허용하는 모드로 설정됐다.
CDS Test Double
CDS는 cl_cds_test_environment를 사용합니다. create_for_multiple_cds( ... )로 여러 CDS를 한 번에 등록할 수 있고, 의존하는 베이스 테이블에 insert_test_data로 데이터를 넣으면 CDS 뷰가 그 데이터로 평가됩니다. 7.52 이상 권장입니다.
정적 메서드는?
맞습니다. cl_abap_testdouble은 인터페이스와 추상 클래스의 인스턴스 메서드만 가짜로 만들 수 있습니다. 정적 의존성을 만나면 "Static을 인스턴스 어댑터로 감싸기"부터 진행해야 합니다 — 이것이 곧 6번 섹션의 inject 패턴입니다.
의존성 주입 패턴 (Default + Inject)
레거시 코드에서 가장 효과적인 리팩터링은 의존성을 생성자나 setter로 외부 주입하도록 바꾸는 것입니다.
CLASS zcl_flight_service DEFINITION PUBLIC CREATE PUBLIC.
PUBLIC SECTION.
METHODS constructor
IMPORTING io_dao TYPE REF TO zif_flight_dao OPTIONAL.
PRIVATE SECTION.
DATA mo_dao TYPE REF TO zif_flight_dao.
ENDCLASS.
CLASS zcl_flight_service IMPLEMENTATION.
METHOD constructor.
mo_dao = COND #( WHEN io_dao IS BOUND
THEN io_dao
ELSE NEW zcl_flight_dao_db( ) ).
ENDMETHOD.
ENDCLASS.
운영에서는 인자를 비우면 실제 DB DAO가 주입되고, 테스트에서는 Test Double을 주입할 수 있습니다. 이를 "Default + Inject" 패턴이라 부르며, ABAP Clean Code 가이드에서도 권장하는 방식입니다.
더 파볼 주제
- CDS Test Double:
cl_cds_test_environment로 분석 뷰까지 단위테스트하기. - Authority Check Test Double:
cl_auth_check_test_env로 권한 체크 분기 검증. - RAP 단위테스트: Behavior Definition의 액션/검증을
cl_abap_behv_test_environment로 격리. - ATC + ABAP Unit 통합: 코드 인스펙터 변형(Variant)에 단위테스트 통과를 강제.
- TDD on ABAP: Red-Green-Refactor 사이클을 ADT의 Q 단축키로 빠르게 돌리기.
더 읽어볼 자료
- SAP Help — ABAP Unit Overview (ABAP Platform)
- SAP Help — Test Doubles for ABAP (CL_ABAP_TESTDOUBLE)
- SAP Help — SQL Test Double Framework (CL_OSQL_TEST_ENVIRONMENT)
- SAP Clean ABAP Style Guide — Testing 섹션
- SAP Community — ABAP Unit 태그
- SAP Help — CDS Test Double Framework
- 도서: xUnit Test Patterns, Gerard Meszaros — Mock/Stub/Spy 분류의 원전
이 글의 코드는 NetWeaver 7.51 / S/4HANA 1909 기준이며, ABAP Cloud 환경에서는 일부 클래스 가시성과 RAP 통합 방식이 다를 수 있어 SAP Help의 최신 버전 문서를 함께 확인하는 것이 일반적으로 권장됩니다.
댓글 0
아직 댓글이 없습니다.