ABAP

ABAP Unit Test 없이 개발하면 그만 #shorts #SAP #ABAP

▶ YouTube에서 보기

1. 개요 및 이 글에서 다루는 범위

ABAP 개발 현장에서 "테스트는 나중에", "DB에 값 넣고 눈으로 확인하면 되지"라는 관성은 여전히 강합니다. 그러나 S/4HANA 전환과 Clean Core 원칙이 확산되면서, DB 의존 없는 단위 테스트가 품질 게이트의 표준으로 자리잡고 있습니다. 이 글은 ABAP Unit Test Framework와 Test Double(테스트 대역) 패턴을 활용해, 데이터베이스·RFC·외부 시스템 의존을 격리한 채로 비즈니스 로직을 검증하는 실전 기법을 다룹니다.

  • ABAP Unit의 실행 원리와 CL_ABAP_UNIT_ASSERT 검증 API 이해
  • Test Double의 4가지 유형(Dummy, Stub, Spy, Mock) 구분
  • Test Seam·Local Test Class·Test Injection 실전 활용
  • SQL Test Double Framework(CDS Test Double) 적용 시나리오
  • 레거시 코드를 테스트 가능한 구조로 리팩터링하는 절차

2. 사전에 알아둘 것

이 글은 ABAP OO(클래스, 인터페이스, 예외 클래스), 로컬 클래스(Local Class) 정의, SE80/ADT(Eclipse) 사용에 익숙한 개발자를 대상으로 합니다. 추가로 ABAP CDS View 기본 문법, RAP(RESTful ABAP Programming Model)의 Behavior Definition 개념, 그리고 의존성 주입(Dependency Injection)의 일반 원리를 알고 있으면 3단계 예제까지 무리 없이 따라올 수 있습니다.

3. 환경 및 준비물

실전 예제는 다음 환경을 기준으로 검증되었습니다.

  • 플랫폼: SAP S/4HANA 2023 On-Premise 또는 SAP BTP ABAP Environment(Steampunk) 2402 릴리스
  • IDE: ABAP Development Tools for Eclipse 3.36 이상 (ABAP Unit Runner 지원)
  • ABAP 릴리스: 7.57 SP01 이상 권장 (SQL Test Double, CDS Test Double 활용 가능)
  • 부가 도구: ABAP Test Cockpit(ATC), Code Coverage Analyzer(SCOV), abapGit(테스트 프로젝트 형상관리)
  • 권한: S_DEVELOP(테스트 클래스 생성), 개발 시스템 사용자

Steampunk의 경우 오픈 SQL 이외 접근이 제한되므로 Test Double 패턴이 사실상 유일한 검증 수단입니다. 예제 코드는 두 환경 모두에서 동작하도록 표준 ABAP OO 문법만 사용합니다.

4. Test Double의 핵심 개념과 동작 원리

Test Double은 영화의 스턴트 대역에서 유래한 용어로, 실제 협력 객체(collaborator)를 대신해 테스트 상황에서 특정 행동을 흉내내는 객체를 말합니다. Gerard Meszaros가 정리한 4가지 유형을 ABAP 관점에서 재해석하면 다음과 같습니다.

  • Dummy: 파라미터 슬롯을 채우기 위해서만 존재. 실제 호출되지 않음. 예: 로그를 남기지 않는 시나리오에서 넘기는 빈 Logger 인스턴스
  • Stub: 미리 정해진 응답을 반환. 예: DB에서 특정 자재코드 조회 시 항상 "재고 100EA"를 리턴하도록 설정한 IF_STOCK_READER 구현체
  • Spy: Stub의 역할에 더해, 호출 횟수·파라미터를 기록. 예: BAL_LOG_MSG_ADD가 몇 번 호출되었는지 검증
  • Mock: 호출 순서·횟수·인자에 대한 사전 기대(expectation)를 정의하고, 미충족 시 테스트 실패. 가장 엄격한 형태

ABAP은 Mockito 같은 동적 목킹 라이브러리가 표준으로 없기 때문에, 실무에서는 "로컬 테스트 클래스가 프로덕션 인터페이스를 구현"하는 정적 방식이 가장 자주 쓰입니다. Steampunk에는 최근 ABAP Cloud Test Double Framework가 도입되어 동적 목킹도 부분적으로 가능해졌습니다.

도식으로 보면, 프로덕션 코드는 인터페이스(IF_ORDER_REPOSITORY)에만 의존하고, 실제 구현(CL_ORDER_REPOSITORY_DB)은 SELECT문을 실행하지만, 테스트 시에는 대역(LTC_ORDER_REPOSITORY_DOUBLE)이 in-memory 테이블을 반환하는 구조입니다. 이때 프로덕션 코드에 대역을 밀어 넣는 방법으로 생성자 주입, Setter 주입, Test Seam, Test Injection(FRIENDS) 4가지가 사용됩니다.

5. 실전 예제 3단계

5-1. 1단계 — 인터페이스 기반 Stub으로 DB 격리

판매오더 할인 계산 로직을 예로 들겠습니다. 원본 클래스는 DB에서 고객 등급을 조회해 할인율을 결정합니다. 이 SELECT 호출을 인터페이스 뒤로 숨긴 뒤, 로컬 테스트 클래스가 Stub을 구현합니다.

INTERFACE if_customer_grade_reader.
  METHODS read_grade
    IMPORTING iv_customer_id  TYPE kunnr
    RETURNING VALUE(rv_grade) TYPE char2
    RAISING   cx_sy_itab_line_not_found.
ENDINTERFACE.

CLASS cl_sales_discount_calc DEFINITION PUBLIC CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS constructor
      IMPORTING io_grade_reader TYPE REF TO if_customer_grade_reader.
    METHODS calc_discount_rate
      IMPORTING iv_customer_id TYPE kunnr
      RETURNING VALUE(rv_rate) TYPE p LENGTH 5 DECIMALS 2.
  PRIVATE SECTION.
    DATA mo_grade_reader TYPE REF TO if_customer_grade_reader.
ENDCLASS.

CLASS cl_sales_discount_calc IMPLEMENTATION.
  METHOD constructor.
    mo_grade_reader = io_grade_reader.
  ENDMETHOD.
  METHOD calc_discount_rate.
    DATA(lv_grade) = mo_grade_reader->read_grade( iv_customer_id ).
    rv_rate = SWITCH #( lv_grade
                       WHEN 'A1' THEN '15.00'
                       WHEN 'B2' THEN '7.50'
                       ELSE '0.00' ).
  ENDMETHOD.
ENDCLASS.

이제 로컬 테스트 클래스에서 Stub을 만들고 검증합니다.

CLASS ltc_discount_test DEFINITION FINAL FOR TESTING
  DURATION SHORT RISK LEVEL HARMLESS.
  PRIVATE SECTION.
    METHODS grade_a1_returns_15 FOR TESTING RAISING cx_static_check.
ENDCLASS.

CLASS ltd_grade_reader_stub DEFINITION FOR TESTING.
  PUBLIC SECTION.
    INTERFACES if_customer_grade_reader.
    DATA mv_grade_to_return TYPE char2.
ENDCLASS.

CLASS ltd_grade_reader_stub IMPLEMENTATION.
  METHOD if_customer_grade_reader~read_grade.
    rv_grade = mv_grade_to_return.
  ENDMETHOD.
ENDCLASS.

CLASS ltc_discount_test IMPLEMENTATION.
  METHOD grade_a1_returns_15.
    DATA(lo_stub) = NEW ltd_grade_reader_stub( ).
    lo_stub->mv_grade_to_return = 'A1'.
    DATA(lo_cut) = NEW cl_sales_discount_calc( lo_stub ).
    cl_abap_unit_assert=>assert_equals(
      exp = CONV #( '15.00' )
      act = lo_cut->calc_discount_rate( '0000010001' ) ).
  ENDMETHOD.
ENDCLASS.

DB 접근이 전혀 없으므로 실행 시간은 밀리초 단위이며, 어떤 개발 시스템에서도 재현 가능합니다.

5-2. 2단계 — Spy로 로깅·예외 경로 검증

실무에서는 정상 케이스만 검증해서는 부족합니다. "예외가 발생하면 반드시 애플리케이션 로그에 ERROR 레벨로 기록되어야 한다"는 규정을 자동화해봅시다. Logger 협력 객체를 Spy로 만들어 호출 이력을 저장합니다.

INTERFACE if_appl_logger.
  METHODS log_error IMPORTING iv_message TYPE string.
ENDINTERFACE.

CLASS cl_invoice_processor DEFINITION PUBLIC CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS constructor
      IMPORTING io_grade_reader TYPE REF TO if_customer_grade_reader
                io_logger       TYPE REF TO if_appl_logger.
    METHODS process
      IMPORTING iv_customer_id TYPE kunnr
      RETURNING VALUE(rv_ok)   TYPE abap_bool.
  PRIVATE SECTION.
    DATA: mo_reader TYPE REF TO if_customer_grade_reader,
          mo_logger TYPE REF TO if_appl_logger.
ENDCLASS.

CLASS cl_invoice_processor IMPLEMENTATION.
  METHOD constructor.
    mo_reader = io_grade_reader.
    mo_logger = io_logger.
  ENDMETHOD.
  METHOD process.
    TRY.
        mo_reader->read_grade( iv_customer_id ).
        rv_ok = abap_true.
      CATCH cx_sy_itab_line_not_found INTO DATA(lx).
        mo_logger->log_error( |Customer { iv_customer_id } not found: { lx->get_text( ) }| ).
        rv_ok = abap_false.
    ENDTRY.
  ENDMETHOD.
ENDCLASS.

테스트 코드에서는 Spy를 이용해 log_error 호출 횟수와 메시지 내용을 확인합니다.

CLASS ltd_logger_spy DEFINITION FOR TESTING.
  PUBLIC SECTION.
    INTERFACES if_appl_logger.
    DATA: mt_captured  TYPE STANDARD TABLE OF string,
          mv_call_count TYPE i.
ENDCLASS.

CLASS ltd_logger_spy IMPLEMENTATION.
  METHOD if_appl_logger~log_error.
    APPEND iv_message TO mt_captured.
    mv_call_count = mv_call_count + 1.
  ENDMETHOD.
ENDCLASS.

CLASS ltd_reader_raising DEFINITION FOR TESTING.
  PUBLIC SECTION.
    INTERFACES if_customer_grade_reader.
ENDCLASS.

CLASS ltd_reader_raising IMPLEMENTATION.
  METHOD if_customer_grade_reader~read_grade.
    RAISE EXCEPTION TYPE cx_sy_itab_line_not_found.
  ENDMETHOD.
ENDCLASS.

CLASS ltc_invoice_error_path DEFINITION FINAL FOR TESTING
  DURATION SHORT RISK LEVEL HARMLESS.
  PRIVATE SECTION.
    METHODS missing_customer_logs_error FOR TESTING.
ENDCLASS.

CLASS ltc_invoice_error_path IMPLEMENTATION.
  METHOD missing_customer_logs_error.
    DATA(lo_spy) = NEW ltd_logger_spy( ).
    DATA(lo_cut) = NEW cl_invoice_processor(
                        io_grade_reader = NEW ltd_reader_raising( )
                        io_logger       = lo_spy ).

    DATA(lv_result) = lo_cut->process( '0000099999' ).

    cl_abap_unit_assert=>assert_false( lv_result ).
    cl_abap_unit_assert=>assert_equals( exp = 1 act = lo_spy->mv_call_count ).
    cl_abap_unit_assert=>assert_char_cp(
      exp = '*not found*' act = lo_spy->mt_captured[ 1 ] ).
  ENDMETHOD.
ENDCLASS.

5-3. 3단계 — CDS Test Double과 ATC 통합

실제 서비스 계층은 CDS 뷰와 RAP Behavior 구현체를 참조합니다. 이때 CL_CDS_TEST_ENVIRONMENT를 사용하면 DB 테이블을 물리적으로 건드리지 않고 In-memory 방식으로 CDS 뷰의 응답을 조작할 수 있습니다.

CLASS ltc_purchase_order_svc DEFINITION FINAL FOR TESTING
  DURATION SHORT RISK LEVEL HARMLESS.
  PRIVATE SECTION.
    CLASS-DATA go_cds_env TYPE REF TO if_cds_test_environment.
    CLASS-METHODS: class_setup, class_teardown.
    METHODS setup.
    METHODS open_amount_sum_is_calculated FOR TESTING RAISING cx_static_check.
ENDCLASS.

CLASS ltc_purchase_order_svc IMPLEMENTATION.
  METHOD class_setup.
    go_cds_env = cl_cds_test_environment=>create(
                   i_for_entity = 'ZC_PURCHASEORDEROPENITEM' ).
  ENDMETHOD.
  METHOD class_teardown.
    go_cds_env->destroy( ).
  ENDMETHOD.
  METHOD setup.
    go_cds_env->clear_doubles( ).
  ENDMETHOD.
  METHOD open_amount_sum_is_calculated.
    DATA lt_doubles TYPE STANDARD TABLE OF zc_purchaseorderopenitem.
    lt_doubles = VALUE #(
      ( purchaseorder = '4500000001' openamount = '1000.00' currency = 'EUR' )
      ( purchaseorder = '4500000002' openamount = '2500.50' currency = 'EUR' ) ).
    go_cds_env->insert_test_data( lt_doubles ).

    DATA(lo_cut) = NEW cl_po_open_summary( ).
    DATA(lv_total) = lo_cut->sum_open_amount( iv_currency = 'EUR' ).

    cl_abap_unit_assert=>assert_equals(
      exp = CONV decfloat34( '3500.50' )
      act = lv_total ).
  ENDMETHOD.
ENDCLASS.

프로덕션 관점의 3단계 체크리스트는 다음과 같습니다.

  • Risk LevelDuration을 반드시 명시 (HARMLESS/SHORT가 CI 파이프라인 기본값)
  • Class-level setup에서 환경을 한 번만 생성하고, 각 테스트 시작 시 clear_doubles로 격리
  • ATC 체크에 "ABAP Unit Test 존재 여부"·"Code Coverage 60% 이상" 규칙을 추가해 커밋 게이트로 활용
  • 보안 관점: 대역 클래스에는 실 데이터·인증 토큰을 포함하지 않고, 필요 시 cl_abap_random으로 합성 데이터 생성

6. 자주 겪는 실수와 트러블슈팅

Q1. "테스트가 개발 시스템에서만 통과하고 CI에서 실패합니다." 대개 원인은 대역이 완전하지 않아 실제 DB SELECT가 남아 있기 때문입니다. CL_OSQL_TEST_ENVIRONMENT로 오픈 SQL을 격리하거나, 프로덕션 코드에서 SELECT를 리포지토리 클래스로 뽑아내 인터페이스 뒤로 숨겨야 합니다.

Q2. "PRIVATE 메서드를 테스트하고 싶은데 접근이 안 됩니다." Local Test Class를 LOCAL FRIENDS로 선언하면 됩니다. 다만 지나친 friends 사용은 캡슐화를 깨뜨리므로, 가급적 public API를 통해 간접 검증하고 friends는 예외적으로만 사용하는 것이 권장됩니다.

Q3. "레거시 함수 모듈은 인터페이스가 없는데 어떻게 대역을 만드나요?" Test Seam(ABAP 7.52+)을 사용하면 프로덕션 코드에 TEST-SEAM ... END-TEST-SEAM 블록을 만들고, 테스트 코드에서 TEST-INJECTION으로 대체 로직을 주입할 수 있습니다. 리팩터링 여유가 없을 때 임시 방편으로 유용합니다.

이 밖에 흔한 실수로는 (1) SETUP 대신 CLASS_SETUP에 상태 있는 대역을 두어 테스트 간 오염 발생, (2) assert_equals에서 타입 불일치로 인해 항상 실패, (3) 예외 검증을 TRY/CATCH로 감싸면서 CATCH가 비어 있어 실제로는 검증이 안 되는 경우 등이 있습니다. 예외는 cl_abap_unit_assert=>assert_that나 명시적 fail( ) 호출로 검증하는 습관이 좋습니다.

7. 이후에 파볼 만한 주제

Test Double 패턴에 익숙해졌다면 다음 영역으로 확장해볼 수 있습니다. 첫째, RAP Behavior의 CL_ABAP_BEHV_TEST_ENVIRONMENT를 활용한 EML(Entity Manipulation Language) 시나리오 테스트. 둘째, Steampunk에서 제공되는 ABAP Cloud Test Double Framework(동적 목킹). 셋째, ATC 커스텀 체크로 "테스트 없이 커밋 불가" 규칙을 조직 표준으로 강제하기. 넷째, Consumer-Driven Contract Test로 확장해 SAP 시스템 간 인터페이스 계약을 검증하는 접근입니다.

8. 더 깊이 파고들 수 있는 자료

  • SAP Help — ABAP Unit Overview (ABAP Platform)
  • SAP Help — ABAP Unit in ADT User Guide
  • SAP Help — Test Double Framework for CDS Entities
  • SAP Community — Writing Testable Code for ABAP
  • SAP Clean ABAP Style Guide — Testing 섹션
  • SAP Help — Testing RAP Business Objects
  • Gerard Meszaros, xUnit Test Patterns — Test Double 분류 원전

댓글 0

아직 댓글이 없습니다.