Test Doubles y Mocking en ABAP Cloud: La guia completa

Kategorie
ABAP-Statements
Veröffentlicht
Autor
Johannes

Los Test Doubles son objetos sustitutos que reemplazan dependencias reales (base de datos, APIs externas, otras clases) en Unit Tests. Permiten tests aislados, rapidos y confiables sin efectos secundarios en sistemas productivos.

El problema: Dependencias en tests

Sin Test Doubles

" ❌ Unit Test problematico
METHOD test_calculate_discount.
" Problema 1: Acceso a BD en test
SELECT SINGLE * FROM zcustomer
WHERE customer_id = '000042'
INTO @DATA(ls_customer).
" Problema 2: Depende de datos en BD
DATA(lo_cut) = NEW zcl_discount_calculator( ).
DATA(lv_discount) = lo_cut->calculate( ls_customer ).
" Problema 3: Test falla si faltan datos
cl_abap_unit_assert=>assert_equals(
exp = 10
act = lv_discount
).
ENDMETHOD.

Problemas:

  • Lento: Acceso a BD toma 50-500ms por test
  • Fragil: Falla si el cliente 000042 no existe
  • Efectos secundarios: Test podria modificar BD
  • No aislado: Testea BD Y logica de negocio simultaneamente

Con Test Doubles

" ✅ Unit Test aislado
METHOD test_calculate_discount.
" Test Double: Fake Customer (sin BD!)
DATA(ls_customer) = VALUE zcustomer(
customer_id = '000042'
customer_class = 'A' " Premium
total_revenue = 100000
).
DATA(lo_cut) = NEW zcl_discount_calculator( ).
DATA(lv_discount) = lo_cut->calculate( ls_customer ).
" Test es rapido, confiable, aislado
cl_abap_unit_assert=>assert_equals(
exp = 15 " Cliente premium → 15% descuento
act = lv_discount
).
ENDMETHOD.

Ventajas:

  • Rapido: 0.1ms en lugar de 50ms
  • Confiable: Datos de test controlados
  • Aislado: Solo se testea logica de negocio
  • Sin efectos secundarios: Sin cambios en BD

Tipos de Test Doubles

┌────────────────────────────────────────────────────────┐
│ Test Doubles │
├────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐│
│ │ Dummy │ │ Stub │ │ Spy │ │ Mock ││
│ └──────────┘ └──────────┘ └──────────┘ └────────┘│
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ Rellenar Devuelve Registra Verifica │
│ parametros valores llamadas comporta-│
│ predefinidos miento │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Fake │ │
│ │ Implementacion funcional (simplificada) │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘

Dummy

Objetos que solo se necesitan para rellenar parametros (valor irrelevante).

METHOD process_order.
IMPORTING io_logger TYPE REF TO zif_logger. " ← Nunca se usa
" Logica de negocio sin Logger
" ...
ENDMETHOD.
" Test:
METHOD test_process_order.
DATA(lo_dummy_logger) = NEW zcl_null_logger( ). " Dummy
mo_cut->process_order( io_logger = lo_dummy_logger ).
" Logger nunca se llama → Dummy es suficiente
ENDMETHOD.

Stub

Devuelve respuestas predefinidas a llamadas de metodos.

" Interface
INTERFACE zif_customer_repository.
METHODS get_customer
IMPORTING iv_id TYPE kunnr
RETURNING VALUE(rs_customer) TYPE zcustomer.
ENDINTERFACE.
" Stub para tests
CLASS zcl_customer_repository_stub DEFINITION.
PUBLIC SECTION.
INTERFACES zif_customer_repository.
DATA ms_customer TYPE zcustomer. " Respuesta predefinida
ENDCLASS.
CLASS zcl_customer_repository_stub IMPLEMENTATION.
METHOD zif_customer_repository~get_customer.
" Siempre misma respuesta, sin importar el ID
rs_customer = ms_customer.
ENDMETHOD.
ENDCLASS.
" Test:
METHOD test_with_premium_customer.
" Arrange: Stub con cliente premium
DATA(lo_stub) = NEW zcl_customer_repository_stub( ).
lo_stub->ms_customer = VALUE #(
customer_id = '000042'
customer_class = 'A'
).
DATA(lo_cut) = NEW zcl_discount_calculator( io_repository = lo_stub ).
" Act
DATA(lv_discount) = lo_cut->calculate_for_customer( '000042' ).
" Assert
cl_abap_unit_assert=>assert_equals( exp = 15 act = lv_discount ).
ENDMETHOD.

Spy

Registra llamadas (cuantas veces, con que parametros).

CLASS zcl_logger_spy DEFINITION.
PUBLIC SECTION.
INTERFACES zif_logger.
DATA mt_logged_messages TYPE string_table. " Registro
ENDCLASS.
CLASS zcl_logger_spy IMPLEMENTATION.
METHOD zif_logger~log.
APPEND iv_message TO mt_logged_messages.
ENDMETHOD.
ENDCLASS.
" Test:
METHOD test_logs_discount_calculation.
" Arrange: Spy
DATA(lo_spy) = NEW zcl_logger_spy( ).
DATA(lo_cut) = NEW zcl_discount_calculator( io_logger = lo_spy ).
" Act
lo_cut->calculate( ... ).
" Assert: ¿Se registro?
cl_abap_unit_assert=>assert_equals(
exp = 1
act = lines( lo_spy->mt_logged_messages )
msg = 'Should log discount calculation'
).
cl_abap_unit_assert=>assert_that(
act = lo_spy->mt_logged_messages[ 1 ]
exp = cl_abap_matcher_text=>contains( 'Discount calculated' )
).
ENDMETHOD.

Mock

Verifica comportamiento (se llamo al metodo con los parametros correctos).

" ABAP no tiene framework de mocking nativo como Mockito (Java)
" → Usar Mock manual o ABAP Test Double Framework
CLASS zcl_email_sender_mock DEFINITION.
PUBLIC SECTION.
INTERFACES zif_email_sender.
DATA mv_send_called TYPE abap_bool.
DATA mv_recipient TYPE string.
DATA mv_subject TYPE string.
ENDCLASS.
CLASS zcl_email_sender_mock IMPLEMENTATION.
METHOD zif_email_sender~send.
mv_send_called = abap_true.
mv_recipient = iv_recipient.
mv_subject = iv_subject.
ENDMETHOD.
ENDCLASS.
" Test:
METHOD test_sends_approval_email.
" Arrange: Mock
DATA(lo_mock) = NEW zcl_email_sender_mock( ).
DATA(lo_cut) = NEW zcl_approval_processor( io_email_sender = lo_mock ).
" Act
lo_cut->approve_order( iv_order_id = '12345' ).
" Assert: Verificar Mock
cl_abap_unit_assert=>assert_true(
act = lo_mock->mv_send_called
msg = 'Should send email on approval'
).
cl_abap_unit_assert=>assert_equals(
act = lo_mock->mv_recipient
msg = 'Should send to manager'
).
cl_abap_unit_assert=>assert_that(
act = lo_mock->mv_subject
exp = cl_abap_matcher_text=>contains( '12345' )
msg = 'Subject should contain order ID'
).
ENDMETHOD.

Fake

Implementacion funcional pero simplificada.

" Produccion: Repository basado en BD
CLASS zcl_customer_repository DEFINITION.
PUBLIC SECTION.
INTERFACES zif_customer_repository.
ENDCLASS.
CLASS zcl_customer_repository IMPLEMENTATION.
METHOD zif_customer_repository~get_customer.
SELECT SINGLE * FROM zcustomer
WHERE customer_id = @iv_id
INTO @rs_customer.
ENDMETHOD.
ENDCLASS.
" Fake: Repository en memoria para tests
CLASS zcl_customer_repository_fake DEFINITION.
PUBLIC SECTION.
INTERFACES zif_customer_repository.
DATA mt_customers TYPE STANDARD TABLE OF zcustomer WITH KEY customer_id.
ENDCLASS.
CLASS zcl_customer_repository_fake IMPLEMENTATION.
METHOD zif_customer_repository~get_customer.
" Fake usa tabla interna en lugar de BD
rs_customer = VALUE #( mt_customers[ customer_id = iv_id ] OPTIONAL ).
ENDMETHOD.
ENDCLASS.
" Test:
METHOD test_with_multiple_customers.
" Arrange: Fake con datos de test
DATA(lo_fake) = NEW zcl_customer_repository_fake( ).
lo_fake->mt_customers = VALUE #(
( customer_id = '001' customer_class = 'A' )
( customer_id = '002' customer_class = 'B' )
( customer_id = '003' customer_class = 'C' )
).
DATA(lo_cut) = NEW zcl_customer_manager( io_repository = lo_fake ).
" Act & Assert
DATA(lv_count) = lo_cut->count_premium_customers( ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lv_count ).
ENDMETHOD.

CDS Test Environment (para RAP/CDS)

Setup

CLASS ltc_travel DEFINITION FINAL FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
CLASS-DATA mo_environment TYPE REF TO if_cds_test_environment.
CLASS-DATA mo_sql_test_environment TYPE REF TO if_osql_test_environment.
CLASS-METHODS:
class_setup,
class_teardown.
METHODS:
setup,
teardown,
test_create_travel FOR TESTING,
test_validate_dates FOR TESTING.
ENDCLASS.

Crear Test Environment

METHOD class_setup.
" CDS Test Environment para RAP Business Objects
mo_environment = cl_cds_test_environment=>create_for_multiple_cds(
i_for_entities = VALUE #(
( i_for_entity = 'ZI_Travel' )
( i_for_entity = 'ZI_Booking' )
( i_for_entity = 'ZI_Customer' ) " ¡Dependencias!
)
).
" Alternativa: SQL Test Environment (para tablas)
mo_sql_test_environment = cl_osql_test_environment=>create(
i_dependency_list = VALUE #(
( 'ZTRAVEL' )
( 'ZBOOKING' )
)
).
ENDMETHOD.

Insertar datos de test

METHOD setup.
" Por test: Datos de test frescos
mo_environment->clear_doubles( ).
" Datos de test para ZI_Customer (¡dependencia!)
mo_environment->insert_test_data(
i_data = VALUE i_customer(
( Customer = '000042' CustomerName = 'Juan Garcia' CustomerClass = 'A' )
( Customer = '000099' CustomerName = 'Maria Lopez' CustomerClass = 'B' )
)
).
" Datos de test para ZI_Travel
mo_environment->insert_test_data(
i_data = VALUE zi_travel(
( TravelId = '00000001'
CustomerId = '000042'
AgencyId = '000001'
BeginDate = '20250601'
EndDate = '20250615'
Status = 'O' )
)
).
ENDMETHOD.

Test con EML

METHOD test_create_travel.
" Arrange: Datos de test ya en setup
" Act: CREATE via EML
MODIFY ENTITIES OF zi_travel
ENTITY Travel
CREATE FIELDS ( CustomerId AgencyId BeginDate EndDate )
WITH VALUE #(
( %cid = 'T1'
CustomerId = '000042'
AgencyId = '000001'
BeginDate = '20250701'
EndDate = '20250715' )
)
MAPPED DATA(mapped)
FAILED DATA(failed)
REPORTED DATA(reported).
COMMIT ENTITIES
RESPONSE OF zi_travel
FAILED DATA(commit_failed)
REPORTED DATA(commit_reported).
" Assert: Sin error
cl_abap_unit_assert=>assert_initial(
act = commit_failed-travel
msg = 'Travel creation should succeed'
).
" Assert: Travel fue creado (via Read)
READ ENTITIES OF zi_travel
ENTITY Travel
ALL FIELDS
WITH VALUE #( ( %cid = 'T1' ) )
RESULT DATA(lt_travel).
cl_abap_unit_assert=>assert_not_initial(
act = lt_travel
msg = 'Travel should exist after creation'
).
cl_abap_unit_assert=>assert_equals(
exp = '000042'
act = lt_travel[ 1 ]-CustomerId
msg = 'Customer ID should match'
).
ENDMETHOD.

Testear validaciones

METHOD test_validate_dates.
" Arrange: EndDate < BeginDate (¡ERROR!)
" Act
MODIFY ENTITIES OF zi_travel
ENTITY Travel
CREATE FIELDS ( BeginDate EndDate )
WITH VALUE #(
( %cid = 'T1'
BeginDate = '20250615' " 15 Junio
EndDate = '20250601' " 1 Junio → ¡INCORRECTO!
CustomerId = '000042'
AgencyId = '000001' )
)
FAILED DATA(failed).
COMMIT ENTITIES
RESPONSE OF zi_travel
FAILED DATA(commit_failed)
REPORTED DATA(commit_reported).
" Assert: Error esperado
cl_abap_unit_assert=>assert_not_initial(
act = commit_failed-travel
msg = 'Validation should fail for invalid dates'
).
" Assert: Mensaje de error presente
cl_abap_unit_assert=>assert_bound(
act = commit_reported-travel[ 1 ]-%msg
msg = 'Error message should be present'
).
" Assert: Campo EndDate marcado
cl_abap_unit_assert=>assert_equals(
exp = if_abap_behv=>mk-on
act = commit_reported-travel[ 1 ]-%element-EndDate
msg = 'EndDate field should be marked as error'
).
ENDMETHOD.

Teardown

METHOD teardown.
" Despues de cada test: Limpiar
ROLLBACK ENTITIES.
mo_environment->clear_doubles( ).
ENDMETHOD.
METHOD class_teardown.
" Despues de todos los tests: Destruir Environment
mo_environment->destroy( ).
mo_sql_test_environment->destroy( ).
ENDMETHOD.

Dependency Injection

Problema: Acoplamiento fuerte

" ❌ No testeable: Dependencia hardcodeada
CLASS zcl_order_processor DEFINITION.
PRIVATE SECTION.
METHODS process_order IMPORTING iv_order_id TYPE vbeln.
ENDCLASS.
CLASS zcl_order_processor IMPLEMENTATION.
METHOD process_order.
" Problema: Dependencia directa de BD
SELECT SINGLE * FROM vbak WHERE vbeln = @iv_order_id INTO @DATA(ls_order).
" Problema: Acceso directo al Logger
zcl_logger=>log( |Processing order { iv_order_id }| ).
" Logica de negocio
" ...
ENDMETHOD.
ENDCLASS.
" Unit Test imposible:
" - Necesita BD real con datos de test
" - Logger escribe en tabla de log productiva

Solucion: Constructor Injection

" ✅ Testeable: Dependencias como Interfaces
INTERFACE zif_order_repository.
METHODS get_order
IMPORTING iv_id TYPE vbeln
RETURNING VALUE(rs_order) TYPE vbak.
ENDINTERFACE.
INTERFACE zif_logger.
METHODS log IMPORTING iv_message TYPE string.
ENDINTERFACE.
CLASS zcl_order_processor DEFINITION.
PUBLIC SECTION.
METHODS constructor
IMPORTING
io_repository TYPE REF TO zif_order_repository
io_logger TYPE REF TO zif_logger.
METHODS process_order
IMPORTING iv_order_id TYPE vbeln.
PRIVATE SECTION.
DATA mo_repository TYPE REF TO zif_order_repository.
DATA mo_logger TYPE REF TO zif_logger.
ENDCLASS.
CLASS zcl_order_processor IMPLEMENTATION.
METHOD constructor.
mo_repository = io_repository.
mo_logger = io_logger.
ENDMETHOD.
METHOD process_order.
" Dependency Injection: Usar dependencias inyectadas
DATA(ls_order) = mo_repository->get_order( iv_order_id ).
mo_logger->log( |Processing order { iv_order_id }| ).
" Logica de negocio (¡ahora testeable!)
" ...
ENDMETHOD.
ENDCLASS.

Unit Test con DI

CLASS ltc_order_processor DEFINITION FINAL FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO zcl_order_processor.
DATA mo_repository_stub TYPE REF TO zcl_order_repository_stub.
DATA mo_logger_spy TYPE REF TO zcl_logger_spy.
METHODS:
setup,
test_process_order FOR TESTING.
ENDCLASS.
CLASS ltc_order_processor IMPLEMENTATION.
METHOD setup.
" Crear Stubs & Spies
mo_repository_stub = NEW zcl_order_repository_stub( ).
mo_logger_spy = NEW zcl_logger_spy( ).
" Class Under Test con Test Doubles
mo_cut = NEW zcl_order_processor(
io_repository = mo_repository_stub
io_logger = mo_logger_spy
).
ENDMETHOD.
METHOD test_process_order.
" Arrange: Stub con datos de test
mo_repository_stub->ms_order = VALUE #(
vbeln = '0000012345'
kunnr = '000042'
).
" Act
mo_cut->process_order( iv_order_id = '0000012345' ).
" Assert: Logger fue llamado (Spy)
cl_abap_unit_assert=>assert_not_initial(
act = mo_logger_spy->mt_logged_messages
msg = 'Logger should be called'
).
cl_abap_unit_assert=>assert_that(
act = mo_logger_spy->mt_logged_messages[ 1 ]
exp = cl_abap_matcher_text=>contains( '0000012345' )
msg = 'Log should contain order ID'
).
ENDMETHOD.
ENDCLASS.

Test Seams (para codigo legacy)

Problema: Codigo legacy sin DI no es testeable.

Solucion: Test Seams = puntos de inyeccion para tests.

Ejemplo: Codigo no testeable

" ❌ Codigo legacy: Acceso a BD directo en codigo
CLASS zcl_legacy_processor DEFINITION.
PUBLIC SECTION.
METHODS calculate_discount
IMPORTING iv_customer_id TYPE kunnr
RETURNING VALUE(rv_discount) TYPE p.
ENDCLASS.
CLASS zcl_legacy_processor IMPLEMENTATION.
METHOD calculate_discount.
" Dependencia directa de BD
SELECT SINGLE customer_class FROM zcustomer
WHERE customer_id = @iv_customer_id
INTO @DATA(lv_class).
" Logica de negocio
rv_discount = COND #(
WHEN lv_class = 'A' THEN 15
WHEN lv_class = 'B' THEN 10
ELSE 5
).
ENDMETHOD.
ENDCLASS.

Insertar Test Seam

" ✅ Con Test Seam
CLASS zcl_legacy_processor IMPLEMENTATION.
METHOD calculate_discount.
DATA lv_class TYPE zclass.
" Test Seam: Puede reemplazarse en tests
TEST-SEAM customer_lookup.
SELECT SINGLE customer_class FROM zcustomer
WHERE customer_id = @iv_customer_id
INTO @lv_class.
END-TEST-SEAM.
" Logica de negocio (¡ahora testeable!)
rv_discount = COND #(
WHEN lv_class = 'A' THEN 15
WHEN lv_class = 'B' THEN 10
ELSE 5
).
ENDMETHOD.
ENDCLASS.

Test con Test Seam

CLASS ltc_legacy_processor DEFINITION FINAL FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO zcl_legacy_processor.
METHODS:
setup,
test_premium_customer FOR TESTING,
test_standard_customer FOR TESTING.
ENDCLASS.
CLASS ltc_legacy_processor IMPLEMENTATION.
METHOD setup.
mo_cut = NEW zcl_legacy_processor( ).
ENDMETHOD.
METHOD test_premium_customer.
" Arrange: Test Seam Injection
TEST-INJECTION customer_lookup.
lv_class = 'A'. " Premium
END-TEST-INJECTION.
" Act
DATA(lv_discount) = mo_cut->calculate_discount( '000042' ).
" Assert
cl_abap_unit_assert=>assert_equals(
exp = 15
act = lv_discount
msg = 'Premium customer should get 15% discount'
).
ENDMETHOD.
METHOD test_standard_customer.
" Arrange
TEST-INJECTION customer_lookup.
lv_class = 'C'. " Standard
END-TEST-INJECTION.
" Act
DATA(lv_discount) = mo_cut->calculate_discount( '000099' ).
" Assert
cl_abap_unit_assert=>assert_equals(
exp = 5
act = lv_discount
msg = 'Standard customer should get 5% discount'
).
ENDMETHOD.
ENDCLASS.

Notas importantes / Mejores practicas

  • Test Doubles > Dependencias reales: Siempre usar Doubles para base de datos, HTTP, Email
  • Dependency Injection: Patron de diseno #1 para codigo testeable
  • CDS Test Environment: Must-Have para tests RAP/CDS
  • Diseno basado en Interfaces: Cada dependencia como Interface → intercambiable
  • Test-First: TDD (Test-Driven Development) recomendado
  • 1 Test = 1 Asercion: Enfocado, facil de depurar
  • Patron AAA: Arrange → Act → Assert (bien estructurado)
  • Nomenclatura: test_<escenario> (ej. test_approve_travel_with_valid_status)
  • Rapido y aislado: Tests deben ejecutarse en < 1 segundo
  • Sin efectos secundarios: Tests no deben escribir en BD/sistema de archivos
  • Test Seams como solucion temporal: Solo para codigo legacy, codigo nuevo = DI
  • Coverage >= 80%: Minimo para codigo productivo
  • Continuo: Tests ejecutandose en cada commit (CI/CD)

Recursos adicionales