ABAP

DB 없이 ABAP 단위테스트? Mock Framework 끝 #shorts #SAP #ABAP

▶ YouTube에서 보기

이 글에서 다룰 것

학습 후 다음 항목을 수행할 수 있습니다.

  • 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_testdoubleconfigure_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=>createi_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 단축키로 빠르게 돌리기.

더 읽어볼 자료

이 글의 코드는 NetWeaver 7.51 / S/4HANA 1909 기준이며, ABAP Cloud 환경에서는 일부 클래스 가시성과 RAP 통합 방식이 다를 수 있어 SAP Help의 최신 버전 문서를 함께 확인하는 것이 일반적으로 권장됩니다.

댓글 0

아직 댓글이 없습니다.