ABAP

DB 없이 CDS 뷰 테스트 — 3가지 방법 #shorts #SAP #ABAP

개요와 이 글에서 얻어갈 것

ABAP CDS 뷰는 SAP S/4HANA 개발의 심장부에 해당합니다. 하지만 CDS 뷰에 담긴 조인 조건, 필드 계산식, 어소시에이션, Access Control 로직이 복잡해질수록 "실제 DB 데이터로만 검증할 수밖에 없다"는 관성이 생깁니다. 그 결과 회귀 테스트가 힘들어지고, 특정 마스터 데이터가 삭제되면 테스트가 깨지는 취약한 구조가 만들어집니다. 이 글에서는 CDS Test Double Framework를 이용해 실제 DB 테이블 없이 CDS 뷰의 동작을 격리·검증하는 방법을 실전 예제 3단계로 파고듭니다.

  • CDS Test Double Framework의 등장 배경과 내부 동작 방식 이해
  • ABAP Unit + CL_CDS_TEST_ENVIRONMENT를 이용한 격리 테스트 골격 작성
  • 어소시에이션·계산 필드·Access Control까지 포함한 실무 시나리오 검증
  • CI/CD 파이프라인에 안전하게 편입시키기 위한 성능·유지보수 팁

이 글을 시작하기 전에 알아두면 좋은 것

CDS View Entity 또는 DDIC 기반 CDS 뷰 작성 경험, ABAP Unit(FOR TESTING) 기본 문법, 그리고 SQL 조인·집계에 대한 이해가 필요합니다. Test Double이라는 용어(Mock, Stub, Fake의 상위 개념)가 낯설다면 xUnit Patterns의 정의를 한 번 훑어보는 것을 권장합니다. Access Control(DCL) 테스트를 진행하려면 PFCG 역할 관리와 ASSERT_ROLE 개념도 함께 갖춰두면 좋습니다.

실습 환경과 준비물

이 글의 예제는 다음 환경 기준으로 검증했습니다.

  • SAP S/4HANA 2022 이상 온프레미스, 또는 ABAP Cloud (SAP BTP ABAP Environment 2308+)
  • ABAP Development Tools (ADT) for Eclipse 2023-12 이상
  • ABAP Language Version: ABAP for Cloud Development 또는 Standard ABAP
  • 테스트 실행 권한: S_DEVELOP (ACTVT 02, 03) 및 CDS Test Environment 접근 권한
  • ABAP Unit Test 실행 단축키: Ctrl+Shift+F10

NetWeaver 7.51 이하 환경에서는 CL_CDS_TEST_ENVIRONMENT가 존재하지 않거나 제한적이므로 On-Premise 프로젝트의 경우 릴리스 노트를 먼저 확인하시기 바랍니다. 또한 테스트 대상 CDS 뷰가 참조하는 모든 테이블·뷰가 double로 대체 가능한지, CROSS JOIN이나 native HANA 함수를 사용하지는 않는지 사전에 파악해 두는 것이 좋습니다.

Test Double Framework의 동작 원리

CDS Test Double Framework는 "실행 시점에 CDS 뷰가 참조하는 소스 테이블/뷰를 임시 인메모리 대체물로 바꿔치기한다"는 아이디어에서 출발합니다. 비유하자면 연극 무대에서 진짜 배우 대신 대역(Double)이 서는 것과 비슷합니다. 조명·음향·대사(=CDS 뷰의 SQL 로직)는 그대로지만, 배우(=DB 테이블)만 바뀌는 셈이죠.

핵심 개체는 CL_CDS_TEST_ENVIRONMENT입니다. 이 클래스가 CDS 뷰 정의를 파싱해 의존 테이블 목록을 뽑아내고, 각 테이블에 대해 세션 로컬한 doubles를 생성합니다. 테스트 종료 시 자동으로 원복되므로 실제 DB에는 흔적이 남지 않습니다.

내부 흐름을 단계별로 뜯어보면 다음과 같습니다.

  1. Create: create( i_for_entity = 'ZC_SALES_ORDER_KPI' ) 호출 시 프레임워크가 해당 CDS 뷰가 참조하는 베이스 엔티티들을 재귀적으로 추적합니다. VDM Cube/Composite 뷰인 경우 하위 Basic 뷰까지 내려갑니다.
  2. Insert Test Data: insert_test_data( )로 내부 테이블을 통해 시나리오별 가짜 데이터를 주입합니다. 이 데이터는 세션 임시 테이블에 적재되며, 테스트 뷰가 이를 참조하도록 CDS 뷰 정의가 리다이렉트됩니다.
  3. Execute: 일반적인 SELECT ... FROM zc_sales_order_kpi 문을 실행합니다. 애플리케이션 코드는 자신이 실제 DB를 조회하는지, double을 조회하는지 알 필요가 없습니다. 이것이 이 프레임워크의 최대 강점입니다.
  4. Assert: cl_abap_unit_assert로 예상 결과와 비교합니다.
  5. Clear: clear_doubles( ) 또는 클래스 해체 시점에 자동 정리됩니다.

여기서 흔히 오해하는 부분은 "CDS 뷰의 SQL이 재작성된다"는 점입니다. 프레임워크는 뷰의 정의 자체를 조작하지 않으며, 뷰가 참조하는 런타임 엔티티를 세션 스코프에서 대체할 뿐입니다. 그래서 뷰 안의 조인 조건, CASE식, 어그리게이션, 어소시에이션 확장(_Partner.Name)이 실제 프로덕션 조회와 동일하게 동작하도록 보장됩니다.

실전 예제 1단계 — 최소 골격 만들기

실무에서 자주 만드는 "판매 오더 상태별 건수 집계" 뷰를 대상으로 시작하겠습니다. 아래 CDS 뷰는 zsales_hdr 테이블을 소스로 상태 코드별 건수를 집계합니다.

@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Order Status KPI'
define view entity ZC_SALES_ORDER_KPI
  as select from zsales_hdr as hdr
{
  key hdr.status_code               as StatusCode,
      count( distinct hdr.order_id ) as OrderCount,
      sum( hdr.net_amount )          as TotalAmount
}
group by hdr.status_code

이 뷰에 대한 최소 테스트 골격은 다음과 같이 작성합니다.

CLASS ltc_sales_kpi DEFINITION FINAL FOR TESTING
  DURATION SHORT
  RISK LEVEL HARMLESS.

  PRIVATE SECTION.
    CLASS-DATA environment TYPE REF TO if_cds_test_environment.

    CLASS-METHODS: class_setup, class_teardown.
    METHODS:       setup,       teardown,
                   count_by_status FOR TESTING RAISING cx_static_check.
ENDCLASS.

CLASS ltc_sales_kpi IMPLEMENTATION.

  METHOD class_setup.
    environment = cl_cds_test_environment=>create(
      i_for_entity = 'ZC_SALES_ORDER_KPI' ).
  ENDMETHOD.

  METHOD class_teardown.
    environment->destroy( ).
  ENDMETHOD.

  METHOD setup.
    environment->clear_doubles( ).
  ENDMETHOD.

  METHOD teardown.
  ENDMETHOD.

  METHOD count_by_status.
    DATA hdr_doubles TYPE STANDARD TABLE OF zsales_hdr.
    hdr_doubles = VALUE #(
      ( order_id = '4500000001' status_code = 'A' net_amount = '100.00' )
      ( order_id = '4500000002' status_code = 'A' net_amount = '250.00' )
      ( order_id = '4500000003' status_code = 'B' net_amount = '999.00' ) ).

    environment->insert_test_data( hdr_doubles ).

    SELECT StatusCode, OrderCount, TotalAmount
      FROM ZC_SALES_ORDER_KPI
      INTO TABLE @DATA(actual).

    cl_abap_unit_assert=>assert_equals(
      exp = 2
      act = lines( actual ) ).
  ENDMETHOD.

ENDCLASS.

class_setup에서 environment를 한 번만 만들고, 각 테스트 setup에서 clear_doubles로 데이터만 리셋하는 패턴이 일반적으로 권장됩니다. 이 방식이 CDS 파싱 오버헤드를 최소화해 테스트 스위트 전체 실행 속도를 크게 개선하기 때문입니다.

실전 예제 2단계 — 어소시에이션과 계산 필드가 있는 실무 뷰

실제 프로젝트에서는 단일 테이블 집계보다 어소시에이션을 타고 들어가는 케이스가 훨씬 많습니다. 아래 뷰는 판매 오더 헤더에 파트너 마스터(zpartner)를 어소시에이션으로 붙이고, VIP 여부를 CASE식으로 파생시킵니다.

define view entity ZC_SALES_ORDER_ENRICHED
  as select from zsales_hdr as hdr
  association [0..1] to zpartner as _Partner
    on $projection.PartnerId = _Partner.partner_id
{
  key hdr.order_id                   as OrderId,
      hdr.partner_id                 as PartnerId,
      _Partner.partner_name          as PartnerName,
      hdr.net_amount                 as NetAmount,
      case
        when hdr.net_amount > 10000 then 'X'
        else ''
      end                            as IsVipOrder,
      _Partner
}

테스트에서는 두 테이블 모두에 double 데이터를 넣고, 어소시에이션이 정상적으로 해석되는지, VIP 플래그가 규칙대로 세팅되는지를 함께 검증합니다. 또한 실무에서는 테스트 실패 시 원인 파악을 돕는 로깅이 중요하므로 cl_abap_unit_assertmsg 파라미터를 적극 활용합니다.

METHOD vip_flag_and_partner_join.
  DATA(hdr_data) = VALUE STANDARD TABLE OF zsales_hdr(
    ( order_id = '4600000010' partner_id = 'P001' net_amount = '15000.00' )
    ( order_id = '4600000011' partner_id = 'P002' net_amount = '  500.00' ) ).

  DATA(partner_data) = VALUE STANDARD TABLE OF zpartner(
    ( partner_id = 'P001' partner_name = 'Acme Corp' )
    ( partner_id = 'P002' partner_name = 'Bravo Inc' ) ).

  environment->insert_test_data( hdr_data ).
  environment->insert_test_data( partner_data ).

  SELECT OrderId, PartnerName, IsVipOrder
    FROM ZC_SALES_ORDER_ENRICHED
    ORDER BY OrderId
    INTO TABLE @DATA(actual).

  cl_abap_unit_assert=>assert_equals(
    exp = 2
    act = lines( actual )
    msg = |Row count mismatch. Doubles: hdr=2, partner=2| ).

  cl_abap_unit_assert=>assert_equals(
    exp = 'Acme Corp'
    act = actual[ 1 ]-partnername
    msg = |Association _Partner not resolved for order { actual[ 1 ]-orderid }| ).

  cl_abap_unit_assert=>assert_equals(
    exp = 'X'
    act = actual[ 1 ]-isviporder
    msg = |VIP threshold rule broken| ).

  cl_abap_unit_assert=>assert_equals(
    exp = ''
    act = actual[ 2 ]-isviporder ).
ENDMETHOD.

실패 시 msg가 ADT의 ABAP Unit Result 창에 그대로 출력되므로, 다중 테스트가 한꺼번에 실패했을 때 원인을 빠르게 구분할 수 있습니다. 또한 어소시에이션 정의가 잘못된 경우(예: on-condition 필드 오타)에도 이 테스트가 즉시 잡아냅니다.

실전 예제 3단계 — Access Control과 성능·CI를 고려한 프로덕션 레벨

운영 코드에 들어갈 CDS 뷰는 대개 DCL(Access Control) 조건이 붙습니다. 예를 들어 판매 조직(sales_org) 기반 권한을 걸어두었다면, 이를 우회한 채로 조회 로직만 테스트하면 실제 운영에서 다른 결과가 나올 수 있습니다. CL_CDS_TEST_ENVIRONMENT는 이 문제를 Access Control 활성/비활성 모드로 해결합니다.

CLASS ltc_sales_kpi_prod DEFINITION FINAL FOR TESTING
  DURATION SHORT
  RISK LEVEL HARMLESS.

  PRIVATE SECTION.
    CLASS-DATA environment TYPE REF TO if_cds_test_environment.

    CLASS-METHODS class_setup.
    CLASS-METHODS class_teardown.
    METHODS: setup,
             dcl_filter_applied  FOR TESTING RAISING cx_static_check,
             dcl_bypassed_mode   FOR TESTING RAISING cx_static_check.
ENDCLASS.

CLASS ltc_sales_kpi_prod IMPLEMENTATION.

  METHOD class_setup.
    environment = cl_cds_test_environment=>create(
      i_for_entity           = 'ZC_SALES_ORDER_ENRICHED'
      i_dependency_list      = VALUE #( ( name = 'ZSALES_HDR' type = 'TABLE' )
                                       ( name = 'ZPARTNER'    type = 'TABLE' ) ) ).
  ENDMETHOD.

  METHOD class_teardown.
    environment->destroy( ).
  ENDMETHOD.

  METHOD setup.
    environment->clear_doubles( ).
  ENDMETHOD.

  METHOD dcl_filter_applied.
    cl_abap_dyn_prg=>check_whitelist_str(
      val = sy-uname white_list = 'TESTUSER' ).

    DATA(hdr_data) = VALUE STANDARD TABLE OF zsales_hdr(
      ( order_id = '4700000001' sales_org = '1000' partner_id = 'P001' net_amount = '100' )
      ( order_id = '4700000002' sales_org = '2000' partner_id = 'P002' net_amount = '200' ) ).

    environment->insert_test_data( hdr_data ).

    SELECT COUNT(*) FROM ZC_SALES_ORDER_ENRICHED INTO @DATA(cnt).

    cl_abap_unit_assert=>assert_equals(
      exp = 1
      act = cnt
      msg = |DCL should filter out sales_org 2000| ).
  ENDMETHOD.

  METHOD dcl_bypassed_mode.
    environment->enable_double_redirection( ).

    DATA(hdr_data) = VALUE STANDARD TABLE OF zsales_hdr(
      ( order_id = '4700000010' sales_org = '9999' partner_id = 'PX' net_amount = '10' ) ).
    environment->insert_test_data( hdr_data ).

    SELECT COUNT(*) FROM ZC_SALES_ORDER_ENRICHED INTO @DATA(cnt).

    cl_abap_unit_assert=>assert_equals( exp = 1 act = cnt ).
  ENDMETHOD.

ENDCLASS.

프로덕션 관점에서 반드시 지켜야 할 규칙 세 가지가 있습니다.

  • 테스트 격리: DURATION SHORT + RISK LEVEL HARMLESS를 유지해야 gCTS·piper·abapGit 파이프라인에서 자동 실행 가능합니다. DB Commit을 유발하는 코드는 절대 넣지 말아야 합니다.
  • Dependency 명시: i_dependency_list로 참조 엔티티를 명시하면 CDS 파서가 재귀 탐색을 스킵하므로 대형 VDM에서 수 초 단위의 성능 개선이 가능합니다.
  • 보안: DCL 테스트 시 실제 사용자 마스터에 의존하지 않도록 CL_AUNIT_INFO의 사용자 설정 API 또는 인증 mock을 병행하는 것이 안전합니다.

자주 밟는 지뢰와 해결책

Q1. insert_test_data는 성공했는데 SELECT 결과가 0건입니다.

가장 흔한 원인은 (1) CDS 뷰의 WHERE절이나 CASE식이 double 데이터를 실제로는 걸러내고 있거나, (2) 타입 불일치(예: CURR 필드에 dec 값 대입)로 캐스팅이 실패한 경우입니다. @Semantics.amount.currencyCode가 필요한 통화 필드는 통화 코드도 함께 넣어야 합니다. 그리고 CDS 뷰가 client-dependent이면 double 레코드의 MANDTsy-mandt와 일치해야 합니다.

Q2. Native HANA 함수(SERIES_GENERATE_INTEGER 등)를 쓰는 뷰는 테스트 불가한가요?

일반적으로 CDS Test Double Framework는 표준 Open SQL로 리다이렉트 가능한 구문만 지원합니다. AMDP·Table Function을 참조하는 뷰는 double 대체 대상이 아니라 실제로 실행되므로, 해당 부분은 별도 유닛 테스트(AMDP 자체 테스트)로 분리하는 접근을 권장합니다.

Q3. 매 테스트마다 CDS 파싱이 오래 걸립니다.

class_setup에서 environment를 한 번만 생성하고, 각 테스트에서는 clear_doubles만 호출하는 패턴을 사용하세요. 또한 여러 CDS 뷰를 한 클래스에서 다루려 하지 말고 뷰당 테스트 클래스를 분리하는 편이 유지보수 측면에서도 낫습니다. i_dependency_list를 명시하면 파싱 시간이 크게 줄어드는 경우가 많습니다.

그 밖의 지뢰로는 Extension View(뷰 확장)로 필드가 추가된 경우 double 데이터에 그 필드도 반드시 채워야 한다는 점, 그리고 Currency Conversion / Unit Conversion 함수는 실제 환율 테이블 double도 함께 준비해야 한다는 점이 있습니다. tcurr, t006 등이 참조되면 이들 테이블도 double 대상에 포함됩니다.

이 다음에 파볼 만한 것들

CDS Test Double Framework를 익혔다면 자연스럽게 SQL Test Double Framework(CL_OSQL_TEST_ENVIRONMENT)로 확장하는 흐름을 권장합니다. 이는 CDS 뷰가 아닌 ABAP 클래스 안의 Open SQL 문 자체를 격리 테스트하는 데 쓰입니다. 또한 RAP(RESTful Application Programming Model) BO의 Behavior Definition을 테스트하는 CL_ABAP_BEHV_TEST_ENVIRONMENT도 같은 계열의 프레임워크입니다. 세 도구를 조합하면 데이터 계층(CDS) - 로직 계층(Open SQL) - 트랜잭션 계층(RAP)을 모두 격리 테스트할 수 있어, 진정한 의미의 ABAP TDD가 가능해집니다. CI 관점에서는 abapGit + piper + AUnit XML 리포트를 조합해 GitHub Actions/Jenkins에서 자동 실행되도록 파이프라인을 구성하는 것이 다음 단계입니다.

더 읽어볼 만한 자료

  • SAP Help Portal — Test Double Framework for CDS Entities
  • SAP Help Portal — Writing Tests with the CDS Test Double Framework (BTP ABAP Env.)
  • SAP Help Portal — ABAP Unit Overview
  • SAP Community — ABAP 태그 (CDS Test Double 관련 실전 Q&A)
  • SAP Press: "ABAP Unit Testing" (2023) — 6장 CDS Test Double Framework
  • xUnit Test Patterns — Test Double 개념 정의

댓글 0

아직 댓글이 없습니다.