Les Test Doubles sont des objets de substitution qui remplacent dans les tests unitaires les vraies dependances (base de donnees, APIs externes, autres classes). Ils permettent des tests isoles, rapides et fiables sans effets de bord sur les systemes de production.
Le probleme : Les dependances dans les tests
Sans Test Doubles
" Test unitaire problematiqueMETHOD test_calculate_discount. " Probleme 1 : Acces BD dans le test SELECT SINGLE * FROM zcustomer WHERE customer_id = '000042" INTO @DATA(ls_customer).
" Probleme 2 : Depend des donnees BD DATA(lo_cut) = NEW zcl_discount_calculator( ). DATA(lv_discount) = lo_cut->calculate( ls_customer ).
" Probleme 3 : Le test echoue si les donnees manquent cl_abap_unit_assert=>assert_equals( exp = 10 act = lv_discount ).ENDMETHOD.Problemes :
- Lent : L’acces BD prend 50-500ms par test
- Fragile : Echoue si le client 000042 n’existe pas
- Effets de bord : Le test pourrait modifier la BD
- Non isole : Teste la BD ET la logique metier en meme temps
Avec Test Doubles
" Test unitaire isoleMETHOD test_calculate_discount. " Test Double : Fake Customer (pas de 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 rapide, fiable, isole cl_abap_unit_assert=>assert_equals( exp = 15 " Client Premium → 15% de remise act = lv_discount ).ENDMETHOD.Avantages :
- Rapide : 0.1ms au lieu de 50ms
- Fiable : Donnees de test controlees
- Isole : Seule la logique metier est testee
- Pas d’effets de bord : Pas de modifications BD
Types de Test Doubles
┌────────────────────────────────────────────────────────┐│ Test Doubles │├────────────────────────────────────────────────────────┤│ ││ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐││ │ Dummy │ │ Stub │ │ Spy │ │ Mock │││ └──────────┘ └──────────┘ └──────────┘ └────────┘││ │ │ │ │ ││ ▼ ▼ ▼ ▼ ││ Remplir les Retourne des Enregistre Verifie ││ parametres valeurs les appels le ││ predefinies comportement││ ││ ┌──────────────────────────────────────────────────┐ ││ │ Fake │ ││ │ Implementation fonctionnelle (simplifiee) │ ││ └──────────────────────────────────────────────────┘ │└────────────────────────────────────────────────────────┘Dummy
Objets necessaires uniquement pour remplir des parametres (valeur sans importance).
METHOD process_order. IMPORTING io_logger TYPE REF TO zif_logger. " ← Jamais utilise
" Logique metier sans 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 ). " Le logger n'est jamais appele → Dummy suffitENDMETHOD.Stub
Retourne des reponses predefinies aux appels de methodes.
" InterfaceINTERFACE zif_customer_repository. METHODS get_customer IMPORTING iv_id TYPE kunnr RETURNING VALUE(rs_customer) TYPE zcustomer.ENDINTERFACE.
" Stub pour les testsCLASS zcl_customer_repository_stub DEFINITION. PUBLIC SECTION. INTERFACES zif_customer_repository. DATA ms_customer TYPE zcustomer. " Reponse predefinieENDCLASS.
CLASS zcl_customer_repository_stub IMPLEMENTATION. METHOD zif_customer_repository~get_customer. " Toujours la meme reponse, quel que soit l'ID rs_customer = ms_customer. ENDMETHOD.ENDCLASS.
" Test :METHOD test_with_premium_customer. " Arrange : Stub avec client 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
Enregistre les appels (combien de fois, avec quels parametres).
CLASS zcl_logger_spy DEFINITION. PUBLIC SECTION. INTERFACES zif_logger. DATA mt_logged_messages TYPE string_table. " EnregistrementENDCLASS.
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 : A-t-il ete logue ? 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
Verifie le comportement (la methode a-t-elle ete appelee avec les bons parametres).
" ABAP n'a pas de framework de mocking natif comme Mockito (Java)" → Mock manuel ou utiliser 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 : Verifier le 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
Implementation fonctionnelle mais simplifiee.
" Production : Repository base sur la 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 memoire pour les 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. " Le Fake utilise une table interne au lieu de la BD rs_customer = VALUE #( mt_customers[ customer_id = iv_id ] OPTIONAL ). ENDMETHOD.ENDCLASS.
" Test :METHOD test_with_multiple_customers. " Arrange : Fake avec donnees 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 (pour 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.Creer le Test Environment
METHOD class_setup. " CDS Test Environment pour les 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' ) " Dependances ! ) ).
" Alternative : SQL Test Environment (pour les tables) mo_sql_test_environment = cl_osql_test_environment=>create( i_dependency_list = VALUE #( ( 'ZTRAVEL' ) ( 'ZBOOKING' ) ) ).ENDMETHOD.Inserer des donnees de test
METHOD setup. " Par test : Donnees de test fraiches mo_environment->clear_doubles( ).
" Donnees de test pour ZI_Customer (Dependance !) mo_environment->insert_test_data( i_data = VALUE i_customer( ( Customer = '000042' CustomerName = 'Max Mustermann' CustomerClass = 'A' ) ( Customer = '000099' CustomerName = 'Lisa Exemple' CustomerClass = 'B' ) ) ).
" Donnees de test pour 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 avec EML
METHOD test_create_travel. " Arrange : Donnees de test deja dans 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 : Pas d'erreur cl_abap_unit_assert=>assert_initial( act = commit_failed-travel msg = 'Travel creation should succeed" ).
" Assert : Travel a ete cree (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.Tester la validation
METHOD test_validate_dates. " Arrange : EndDate < BeginDate (ERREUR !)
" Act MODIFY ENTITIES OF zi_travel ENTITY Travel CREATE FIELDS ( BeginDate EndDate ) WITH VALUE #( ( %cid = 'T1" BeginDate = '20250615' " 15 juin EndDate = '20250601' " 1er juin → FAUX ! CustomerId = '000042" AgencyId = '000001' ) ) FAILED DATA(failed).
COMMIT ENTITIES RESPONSE OF zi_travel FAILED DATA(commit_failed) REPORTED DATA(commit_reported).
" Assert : Erreur attendue cl_abap_unit_assert=>assert_not_initial( act = commit_failed-travel msg = 'Validation should fail for invalid dates" ).
" Assert : Message d'erreur present cl_abap_unit_assert=>assert_bound( act = commit_reported-travel[ 1 ]-%msg msg = 'Error message should be present" ).
" Assert : Champ EndDate marque 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. " Apres chaque test : Nettoyage ROLLBACK ENTITIES. mo_environment->clear_doubles( ).ENDMETHOD.
METHOD class_teardown. " Apres tous les tests : Detruire l'environnement mo_environment->destroy( ). mo_sql_test_environment->destroy( ).ENDMETHOD.Dependency Injection
Probleme : Couplage fort
" Non testable : Dependance codee en durCLASS zcl_order_processor DEFINITION. PRIVATE SECTION. METHODS process_order IMPORTING iv_order_id TYPE vbeln.ENDCLASS.
CLASS zcl_order_processor IMPLEMENTATION. METHOD process_order. " Probleme : Dependance BD directe SELECT SINGLE * FROM vbak WHERE vbeln = @iv_order_id INTO @DATA(ls_order).
" Probleme : Acces logger direct zcl_logger=>log( |Processing order { iv_order_id }| ).
" Logique metier " ... ENDMETHOD.ENDCLASS.
" Test unitaire impossible :" - Necessite une vraie BD avec des donnees de test" - Le logger ecrit dans la table de log productiveSolution : Constructor Injection
" Testable : Dependances en tant qu'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 : Utiliser les dependances injectees DATA(ls_order) = mo_repository->get_order( iv_order_id ). mo_logger->log( |Processing order { iv_order_id }| ).
" Logique metier (maintenant testable !) " ... ENDMETHOD.
ENDCLASS.Test unitaire avec 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. " Creer Stubs & Spies mo_repository_stub = NEW zcl_order_repository_stub( ). mo_logger_spy = NEW zcl_logger_spy( ).
" Class Under Test avec 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 avec donnees de test mo_repository_stub->ms_order = VALUE #( vbeln = '0000012345" kunnr = '000042" ).
" Act mo_cut->process_order( iv_order_id = '0000012345' ).
" Assert : Le logger a ete appele (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 (pour le code legacy)
Probleme : Le code legacy sans DI n’est pas testable.
Solution : Test Seams = Points d’injection pour les tests.
Exemple : Code non testable
" Code legacy : Acces BD directement dans le codeCLASS 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. " Dependance BD directe SELECT SINGLE customer_class FROM zcustomer WHERE customer_id = @iv_customer_id INTO @DATA(lv_class).
" Logique metier rv_discount = COND #( WHEN lv_class = 'A' THEN 15 WHEN lv_class = 'B' THEN 10 ELSE 5 ). ENDMETHOD.ENDCLASS.Inserer un Test Seam
" Avec Test SeamCLASS zcl_legacy_processor IMPLEMENTATION. METHOD calculate_discount. DATA lv_class TYPE zclass.
" Test Seam : Peut etre remplace dans les tests TEST-SEAM customer_lookup. SELECT SINGLE customer_class FROM zcustomer WHERE customer_id = @iv_customer_id INTO @lv_class. END-TEST-SEAM.
" Logique metier (maintenant testable !) rv_discount = COND #( WHEN lv_class = 'A' THEN 15 WHEN lv_class = 'B' THEN 10 ELSE 5 ). ENDMETHOD.ENDCLASS.Test avec 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.Remarques importantes / Bonnes pratiques
- Test Doubles > Vraies dependances : Toujours utiliser des Doubles pour BD, HTTP, Email
- Dependency Injection : Pattern de design #1 pour du code testable
- CDS Test Environment : Indispensable pour les tests RAP/CDS
- Design base sur les interfaces : Chaque dependance en tant qu’interface → interchangeable
- Test-First : TDD (Test-Driven Development) recommande
- 1 Test = 1 Assertion : Focalise, facile a deboguer
- Pattern AAA : Arrange → Act → Assert (clairement structure)
- Nommage :
test_<scenario>(ex.test_approve_travel_with_valid_status) - Rapide & Isole : Les tests doivent s’executer en < 1 seconde
- Pas d’effets de bord : Les tests ne doivent rien ecrire dans la BD/systeme de fichiers
- Test Seams comme solution de secours : Uniquement pour le code legacy, nouveau code = DI
- Couverture >= 80% : Minimum pour le code productif
- Continu : Tests a chaque commit (CI/CD)
Ressources supplementaires
- ABAP Unit Testing : /abap-unit-testing/
- RAP Basics : /rap-basics/
- Guide EML : /eml-entity-manipulation-language/
- ABAP Cloud : /abap-cloud-definition/