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 problematicoMETHOD 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 aisladoMETHOD 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 suficienteENDMETHOD.Stub
Devuelve respuestas predefinidas a llamadas de metodos.
" InterfaceINTERFACE zif_customer_repository. METHODS get_customer IMPORTING iv_id TYPE kunnr RETURNING VALUE(rs_customer) TYPE zcustomer.ENDINTERFACE.
" Stub para testsCLASS zcl_customer_repository_stub DEFINITION. PUBLIC SECTION. INTERFACES zif_customer_repository. DATA ms_customer TYPE zcustomer. " Respuesta predefinidaENDCLASS.
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. " RegistroENDCLASS.
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 BDCLASS 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 testsCLASS 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 hardcodeadaCLASS 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 productivaSolucion: Constructor Injection
" ✅ Testeable: Dependencias como InterfacesINTERFACE 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 codigoCLASS 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 SeamCLASS 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
- ABAP Unit Testing: /abap-unit-testing/
- RAP Basics: /rap-basics/
- Guia EML: /eml-entity-manipulation-language/
- ABAP Cloud: /abap-cloud-definition/