1. 개요 및 이 글에서 얻어갈 것
ABAP Unit 테스트는 단순히 cl_abap_unit_assert를 호출하는 것만으로 끝나지 않습니다. 동일한 테스트 메서드라도 실행 순서에 따라 결과가 달라지는 경우가 빈번하게 발생하는데, 이를 순서 의존성(Test Order Dependency)이라고 부릅니다. 본 글에서는 ABAP Unit Framework에서 독립 테스트(Independent Test)와 순차 테스트(Sequential Test)의 차이, 그리고 SETUP/TEARDOWN, RISK LEVEL, DURATION 등을 활용해 안정적인 테스트 스위트를 구성하는 방법을 다룹니다.
이 글을 끝까지 읽으면 다음을 점검할 수 있습니다.
- 순서 의존성이 발생하는 원인과 진단 방법
CLASS_SETUP,SETUP,TEARDOWN,CLASS_TEARDOWN4가지 후크의 차이- RISK LEVEL(HARMLESS/DANGEROUS/CRITICAL)과 DURATION(SHORT/MEDIUM/LONG)의 의미
- 주문 처리(Order Processing) 시나리오에서 독립성을 보장하는 패턴
CL_ABAP_TESTDOUBLE를 사용한 Mock 객체 재사용 시 주의점
2. 핵심 개념 — 순서 의존성과 독립 테스트의 차이
독립 테스트란 어떤 순서로 실행되어도 동일한 결과를 보장하는 테스트입니다. ABAP Unit Framework는 일반적으로 테스트 메서드를 임의의 순서로 실행할 수 있다고 가정하므로, 각 테스트 메서드는 외부 상태(데이터베이스, 정적 속성, 글로벌 메모리)에 의존하지 않아야 권장됩니다.
반면 순차 테스트는 특정 순서를 가정합니다. 예를 들어 test_create_order 이후에 test_update_order가 실행되어야만 통과하는 식입니다. 이런 패턴은 처음 작성할 때는 편하지만, 테스트 일부만 실행하거나 병렬 실행 환경으로 옮길 때 즉시 깨집니다.
비유하자면, 독립 테스트는 각자 도시락을 싸 오는 소풍이고, 순차 테스트는 앞 사람이 만든 요리를 다음 사람이 이어 받아 완성하는 릴레이 요리입니다. 누군가 결근하면 릴레이는 무너집니다.
ABAP Unit이 제공하는 안전장치는 크게 세 가지입니다.
- Fixture 후크:
CLASS_SETUP/SETUP/TEARDOWN/CLASS_TEARDOWN으로 각 테스트의 초기/정리 상태를 보장합니다. - 속성 어노테이션:
RISK LEVEL과DURATION을 통해 실행 환경(시스템 영향도, 예상 소요)에 대한 정보를 명시합니다. - Test Double:
CL_ABAP_TESTDOUBLE로 외부 의존성을 가짜 객체로 치환하여, 데이터베이스 상태 의존 문제를 차단합니다.
3. 1단계: 독립 테스트 메서드 작성 (cl_abap_unit_assert)
먼저 가장 기본적인 독립 테스트의 형태를 살펴보겠습니다. 각 메서드는 자신만의 입력을 만들고, 자신만의 단언(assertion)을 수행합니다.
CLASS ltc_order_calculator DEFINITION FINAL
FOR TESTING
RISK LEVEL HARMLESS
DURATION SHORT.
PRIVATE SECTION.
METHODS:
calc_total_with_tax FOR TESTING,
calc_total_zero_qty FOR TESTING,
calc_total_discount FOR TESTING.
ENDCLASS.
CLASS ltc_order_calculator IMPLEMENTATION.
METHOD calc_total_with_tax.
DATA(lo_calc) = NEW zcl_order_calc( ).
DATA(lv_result) = lo_calc->total(
iv_price = 1000
iv_qty = 2
iv_tax = '0.10'
).
cl_abap_unit_assert=>assert_equals(
act = lv_result
exp = 2200
msg = |수량 2개, 세율 10% 일 때 총액은 2200 이어야 합니다| ).
ENDMETHOD.
METHOD calc_total_zero_qty.
DATA(lo_calc) = NEW zcl_order_calc( ).
DATA(lv_result) = lo_calc->total(
iv_price = 9999
iv_qty = 0
iv_tax = '0.10' ).
cl_abap_unit_assert=>assert_equals(
act = lv_result
exp = 0 ).
ENDMETHOD.
METHOD calc_total_discount.
DATA(lo_calc) = NEW zcl_order_calc( ).
DATA(lv_result) = lo_calc->total_with_discount(
iv_price = 1000
iv_qty = 3
iv_discount = '0.20' ).
cl_abap_unit_assert=>assert_equals(
act = lv_result
exp = 2400 ).
ENDMETHOD.
ENDCLASS.
위 세 메서드는 어떤 순서로 호출하든 결과가 동일합니다. NEW zcl_order_calc( )로 매번 새 인스턴스를 만들기 때문에 상태가 누적되지 않습니다. 이것이 독립 테스트의 핵심 패턴입니다.
4. 2단계: SETUP / TEARDOWN으로 공유 상태 초기화
매번 객체를 새로 만드는 것은 안전하지만 반복 코드가 늘어납니다. 공통 픽스처는 SETUP 메서드로 이동시킬 수 있습니다. ABAP Unit Framework는 각 테스트 메서드 실행 직전마다 SETUP을 호출하고, 실행 직후마다 TEARDOWN을 호출합니다. 따라서 인스턴스 속성이라도 매 테스트마다 새로 초기화됩니다.
CLASS ltc_order_service DEFINITION FINAL
FOR TESTING
RISK LEVEL HARMLESS
DURATION SHORT.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO zcl_order_service. " Class Under Test
DATA mo_repo TYPE REF TO zif_order_repository.
METHODS:
setup,
teardown,
create_order_returns_id FOR TESTING,
create_order_persists FOR TESTING,
cancel_order_marks_void FOR TESTING.
ENDCLASS.
CLASS ltc_order_service IMPLEMENTATION.
METHOD setup.
" 매 테스트 직전마다 새 Mock Repository와 새 서비스 인스턴스 생성
mo_repo = CAST zif_order_repository(
cl_abap_testdouble=>create( 'ZIF_ORDER_REPOSITORY' ) ).
mo_cut = NEW zcl_order_service( io_repo = mo_repo ).
ENDMETHOD.
METHOD teardown.
" 명시적 정리 (참조 끊기)
CLEAR: mo_cut, mo_repo.
ENDMETHOD.
METHOD create_order_returns_id.
cl_abap_testdouble=>configure_call( mo_repo )->returning( '4711' ).
mo_repo->next_id( ).
DATA(lv_id) = mo_cut->create_order( iv_customer = '0001' ).
cl_abap_unit_assert=>assert_equals( act = lv_id exp = '4711' ).
ENDMETHOD.
METHOD create_order_persists.
mo_cut->create_order( iv_customer = '0002' ).
cl_abap_testdouble=>verify_expectations( mo_repo ).
ENDMETHOD.
METHOD cancel_order_marks_void.
mo_cut->cancel_order( iv_id = '4711' ).
" Mock에 cancel 호출이 1회 일어났는지 검증
cl_abap_testdouble=>verify_expectations( mo_repo ).
ENDMETHOD.
ENDCLASS.
여기서 주의할 점은 CLASS_SETUP과 SETUP의 차이입니다.
CLASS_SETUP/CLASS_TEARDOWN: 테스트 클래스 전체 실행에 대해 1회만 호출됩니다. 비용이 큰 픽스처(예: 변경되지 않을 상수 테이블 준비)에 적합합니다.SETUP/TEARDOWN: 각 테스트 메서드마다 호출됩니다. 변경 가능한 상태는 여기서 초기화해야 순서 의존성을 차단할 수 있습니다.
5. 3단계: RISK LEVEL / DURATION 설정과 테스트 그룹
ABAP Unit은 테스트 클래스 단위로 시스템 영향도와 예상 실행 시간을 명시할 수 있습니다. 시스템 설정(RSAUNIT_ADMIN 또는 ATC 통합)에서 어떤 RISK LEVEL을 허용할지 결정하므로, 잘못 설정하면 테스트가 아예 실행되지 않을 수 있습니다.
CLASS ltc_order_db_integration DEFINITION FINAL
FOR TESTING
RISK LEVEL DANGEROUS " 실제 테이블 변경 가능
DURATION MEDIUM. " 1분 이내 실행 예상
PRIVATE SECTION.
METHODS:
class_setup FOR TESTING CLASS-METHODS,
class_teardown FOR TESTING CLASS-METHODS,
setup,
teardown,
insert_then_read_back FOR TESTING.
ENDCLASS.
각 값의 의미를 정리하면 다음과 같습니다.
RISK LEVEL HARMLESS: 운영 데이터에 영향이 없는 안전한 단위 테스트. 기본값.RISK LEVEL DANGEROUS: 영속 데이터(예: 커스텀 테이블)에 쓰기를 수행할 수 있는 테스트.RISK LEVEL CRITICAL: 시스템 설정/Customizing까지 변경할 수 있는 테스트. 별도 승인 환경에서만 권장됩니다.DURATION SHORT: 보통 1초 이내. CI에서 빈번하게 실행 가능.DURATION MEDIUM: 1분 내외. 통합 시나리오에 적합.DURATION LONG: 그 이상. 야간 잡으로 분리하여 실행하는 것이 일반적입니다.
RISK LEVEL/DURATION은 순서 의존성을 강제하는 어노테이션이 아닙니다. 다만 통합 테스트처럼 외부 상태에 영향을 주는 그룹은 자연스럽게 순차적으로 묶어 다른 단위 테스트와 분리하는 것이 권장됩니다.
6. 4단계: 실전 예제 — 주문 처리 테스트 독립성 보장
이제 좀 더 현실적인 시나리오를 살펴보겠습니다. 주문 생성, 상태 변경, 취소가 얽혀 있는 서비스 클래스를 테스트한다고 가정합니다. 잘못 작성된 순차 테스트는 다음과 같은 모양입니다.
" 안티 패턴: 순서 의존성에 의존하는 테스트
CLASS ltc_bad_order DEFINITION FINAL FOR TESTING.
PRIVATE SECTION.
CLASS-DATA mv_created_id TYPE string. " ★ 클래스 변수 공유
METHODS:
step1_create FOR TESTING,
step2_update FOR TESTING,
step3_cancel FOR TESTING.
ENDCLASS.
CLASS ltc_bad_order IMPLEMENTATION.
METHOD step1_create.
mv_created_id = NEW zcl_order_service( )->create_order( iv_customer = '0001' ).
cl_abap_unit_assert=>assert_not_initial( mv_created_id ).
ENDMETHOD.
METHOD step2_update.
" ★ step1이 먼저 실행됐다는 가정 — 깨지기 쉬움
NEW zcl_order_service( )->update_order( iv_id = mv_created_id iv_qty = 5 ).
ENDMETHOD.
METHOD step3_cancel.
NEW zcl_order_service( )->cancel_order( iv_id = mv_created_id ).
ENDMETHOD.
ENDCLASS.
이 구조의 문제는 명확합니다. step2_update만 단독으로 실행하면 mv_created_id가 비어 있어 실패합니다. ABAP Unit Framework가 메서드를 알파벳 순으로 실행하기 때문에 우연히 통과해 보일 수 있지만, 리팩토링하면서 메서드 이름을 바꾸는 순간 무너집니다.
독립성을 보장하는 패턴으로 다시 작성하면 다음과 같습니다.
CLASS ltc_good_order DEFINITION FINAL
FOR TESTING
RISK LEVEL HARMLESS
DURATION SHORT.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO zcl_order_service.
DATA mo_repo TYPE REF TO zif_order_repository.
METHODS:
setup,
given_existing_order RETURNING VALUE(rv_id) TYPE string,
create_order_returns_new_id FOR TESTING,
update_order_changes_qty FOR TESTING,
cancel_order_sets_status_x FOR TESTING.
ENDCLASS.
CLASS ltc_good_order IMPLEMENTATION.
METHOD setup.
mo_repo = CAST zif_order_repository(
cl_abap_testdouble=>create( 'ZIF_ORDER_REPOSITORY' ) ).
mo_cut = NEW zcl_order_service( io_repo = mo_repo ).
ENDMETHOD.
METHOD given_existing_order.
" 각 테스트가 필요한 사전 상태를 스스로 준비
cl_abap_testdouble=>configure_call( mo_repo )->returning( '9000' ).
mo_repo->next_id( ).
rv_id = mo_cut->create_order( iv_customer = '0001' ).
ENDMETHOD.
METHOD create_order_returns_new_id.
DATA(lv_id) = given_existing_order( ).
cl_abap_unit_assert=>assert_equals( act = lv_id exp = '9000' ).
ENDMETHOD.
METHOD update_order_changes_qty.
DATA(lv_id) = given_existing_order( ).
mo_cut->update_order( iv_id = lv_id iv_qty = 5 ).
cl_abap_testdouble=>verify_expectations( mo_repo ).
ENDMETHOD.
METHOD cancel_order_sets_status_x.
DATA(lv_id) = given_existing_order( ).
mo_cut->cancel_order( iv_id = lv_id ).
cl_abap_testdouble=>verify_expectations( mo_repo ).
ENDMETHOD.
ENDCLASS.
핵심은 given_existing_order처럼 각 테스트가 필요한 사전 상태를 스스로 준비한다는 점입니다. CLASS-DATA로 ID를 공유하지 않으므로, 어떤 메서드만 골라 실행해도 결과는 동일합니다.
7. 자주 겪는 함정과 FAQ
Q1. 분명히 독립적으로 짰는데 단독 실행과 일괄 실행 결과가 다릅니다.
가장 흔한 원인은 CLASS-DATA나 정적 속성으로 선언된 캐시입니다. 운영 코드에 CLASS-DATA gt_cache가 있다면, 첫 테스트가 채워 둔 캐시를 두 번째 테스트가 그대로 읽어버립니다. 운영 코드 수정이 어렵다면 SETUP에서 zcl_*=>reset_cache( )처럼 명시적인 리셋 API를 호출하거나, 캐시 자체를 의존성 주입 대상으로 분리하는 것을 권장합니다.
Q2. 데이터베이스 상태에 의존하는 테스트는 어떻게 해야 하나요?
두 가지 선택지가 일반적입니다. 첫째, CL_ABAP_TESTDOUBLE 또는 ABAP SQL Test Double Framework(CL_OSQL_TEST_ENVIRONMENT)로 DB 자체를 가짜로 만드는 방법입니다. 둘째, RISK LEVEL DANGEROUS로 명시하고 TEARDOWN에서 ROLLBACK WORK를 호출하거나, 테스트가 끝나기 전 DELETE FROM ztest_table WHERE testrun = sy-uname으로 정리하는 방법입니다. CI 파이프라인에서는 일반적으로 첫 번째가 안정적입니다.
Q3. Mock 객체를 여러 테스트에서 재사용하고 싶은데, 안전한가요?
CL_ABAP_TESTDOUBLE로 만든 인스턴스는 호출 기대값을 내부 상태로 보관합니다. CLASS_SETUP에서 한 번만 만들고 여러 테스트에서 재사용하면, 이전 테스트의 configure_call 설정이 다음 테스트로 흘러들어 가는 사고가 발생합니다. Mock은 SETUP에서 매번 새로 만드는 것이 권장됩니다.
Q4. 테스트 메서드 실행 순서를 강제할 방법이 있나요?
ABAP Unit Framework는 일반적으로 메서드 정의 순서대로 실행하지만, 이는 구현 디테일이며 보장된 계약이 아닙니다. 순서를 강제하고 싶다면 별도의 시나리오 테스트 메서드 1개를 만들고, 그 안에서 단계별 로직을 호출하는 것이 안전합니다. 즉, "Given-When-Then"을 한 메서드 안에서 완결시키는 패턴을 권장합니다.
Q5. RISK LEVEL HARMLESS인데 시스템에서 실행이 차단됩니다.
시스템 프로파일 파라미터(auth/test_mode 등)나 ATC 변형(variant)에서 특정 RISK LEVEL/DURATION만 허용했을 가능성이 큽니다. SAUNIT_CLIENT_SETUP(또는 해당 시스템의 ABAP Unit 관리 트랜잭션)에서 현재 클라이언트의 허용 설정을 확인해 보는 것이 일반적입니다.
8. 응용 패턴 — Test Double / CL_ABAP_TESTDOUBLE과의 결합
독립성과 순서 무관성을 끝까지 밀고 가려면 외부 의존성을 전부 Test Double로 대체하는 패턴을 권장합니다. ABAP에서는 CL_ABAP_TESTDOUBLE이 표준 도구이며, 인터페이스 또는 비-final 클래스에 대해 가짜 구현을 생성할 수 있습니다.
METHOD cancel_propagates_to_payment.
" given
DATA(lo_payment) = CAST zif_payment_gateway(
cl_abap_testdouble=>create( 'ZIF_PAYMENT_GATEWAY' ) ).
cl_abap_testdouble=>configure_call( lo_payment )->returning( abap_true ).
lo_payment->refund( iv_id = '9000' iv_amount = 1000 ).
DATA(lo_cut) = NEW zcl_order_service(
io_repo = mo_repo
io_payment = lo_payment ).
" when
lo_cut->cancel_order( iv_id = '9000' ).
" then
cl_abap_testdouble=>verify_expectations( lo_payment ).
ENDMETHOD.
이 패턴을 정착시키면 다음과 같은 효과를 기대할 수 있습니다.
- 테스트가 데이터베이스/외부 시스템 상태에 영향을 받지 않으므로 순서 의존성이 자연스럽게 사라집니다.
- 각 테스트가 Given-When-Then 구조를 명확히 표현하게 되어 가독성이 개선됩니다.
- 운영 코드가 인터페이스 기반 의존성 주입을 받도록 유도되어, 결합도가 낮아집니다.
- ABAP Cloud 환경에서도 동일한 테스트 자산을 재사용할 수 있는 가능성이 커집니다.
마지막으로 한 가지 권장 사항은, 새로운 테스트를 작성할 때 항상 "이 메서드만 단독으로 실행해도 통과하는가?"를 자문하는 것입니다. ADT(ABAP Development Tools)의 Test Explorer에서는 개별 메서드만 선택해 실행하는 기능이 제공되므로, 작성 직후 단독 실행으로 검증하는 습관을 들이면 순서 의존성에서 비롯되는 대부분의 문제를 사전에 차단할 수 있습니다.
독립 테스트와 순차 테스트는 양자택일의 문제가 아닙니다. 단위 테스트 레벨에서는 독립성을 강하게 유지하고, 통합/시나리오 레벨에서는 의도적으로 순차 흐름을 한 메서드에 모아두는 것이 일반적으로 권장되는 균형입니다. SETUP/TEARDOWN, RISK LEVEL/DURATION, 그리고 Test Double 세 가지를 손에 익히면, 어떤 규모의 ABAP 프로젝트에서도 안정적인 테스트 스위트를 유지할 수 있습니다.
댓글 0
아직 댓글이 없습니다.