ABAP Unit Testing : Développement piloté par les tests

Catégorie
ABAP-Statements
Publié
Auteur
Johannes

ABAP Unit est le framework de test intégré pour les tests unitaires automatisés en ABAP. Il permet le développement piloté par les tests (TDD) et assure la qualité du code grâce à des tests automatisés et reproductibles.

Concept de base

  • Les classes de test sont des classes locales avec l’attribut FOR TESTING
  • Les méthodes de test sont des méthodes avec l’attribut FOR TESTING
  • Les assertions vérifient les résultats attendus avec CL_ABAP_UNIT_ASSERT
  • Les tests sont exécutés via CTRL+SHIFT+F10 ou le menu contextuel

Syntaxe

CLASS ltc_test_class DEFINITION
FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS: test_method FOR TESTING.
ENDCLASS.
CLASS ltc_test_class IMPLEMENTATION.
METHOD test_method.
" Logique de test avec assertions
cl_abap_unit_assert=>assert_equals(
act = actual_value
exp = expected_value
).
ENDMETHOD.
ENDCLASS.

Exemples

1. Test unitaire simple

" Classe à tester
CLASS lcl_calculator DEFINITION.
PUBLIC SECTION.
METHODS: add IMPORTING iv_a TYPE i iv_b TYPE i
RETURNING VALUE(rv_result) TYPE i.
ENDCLASS.
CLASS lcl_calculator IMPLEMENTATION.
METHOD add.
rv_result = iv_a + iv_b.
ENDMETHOD.
ENDCLASS.
" Classe de test
CLASS ltc_calculator DEFINITION
FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA: mo_cut TYPE REF TO lcl_calculator. " CUT = Class Under Test
METHODS:
setup,
test_add_positive_numbers FOR TESTING,
test_add_negative_numbers FOR TESTING,
test_add_zero FOR TESTING.
ENDCLASS.
CLASS ltc_calculator IMPLEMENTATION.
METHOD setup.
" Exécuté avant chaque test
mo_cut = NEW #( ).
ENDMETHOD.
METHOD test_add_positive_numbers.
DATA(lv_result) = mo_cut->add( iv_a = 5 iv_b = 3 ).
cl_abap_unit_assert=>assert_equals(
act = lv_result
exp = 8
msg = 'L''addition de nombres positifs a échoué"
).
ENDMETHOD.
METHOD test_add_negative_numbers.
DATA(lv_result) = mo_cut->add( iv_a = -5 iv_b = -3 ).
cl_abap_unit_assert=>assert_equals(
act = lv_result
exp = -8
).
ENDMETHOD.
METHOD test_add_zero.
DATA(lv_result) = mo_cut->add( iv_a = 10 iv_b = 0 ).
cl_abap_unit_assert=>assert_equals(
act = lv_result
exp = 10
).
ENDMETHOD.
ENDCLASS.

2. Méthodes d’assertion importantes

CLASS ltc_assertions DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS:
test_assert_equals FOR TESTING,
test_assert_true_false FOR TESTING,
test_assert_initial FOR TESTING,
test_assert_bound FOR TESTING,
test_assert_differs FOR TESTING,
test_assert_char_cp FOR TESTING,
test_fail FOR TESTING.
ENDCLASS.
CLASS ltc_assertions IMPLEMENTATION.
METHOD test_assert_equals.
" Comparer des valeurs
cl_abap_unit_assert=>assert_equals(
act = 42
exp = 42
msg = 'Les valeurs ne sont pas égales"
).
" Comparer des structures
DATA: ls_act TYPE ty_person,
ls_exp TYPE ty_person.
ls_act = VALUE #( name = 'Max' age = 30 ).
ls_exp = VALUE #( name = 'Max' age = 30 ).
cl_abap_unit_assert=>assert_equals(
act = ls_act
exp = ls_exp
).
" Comparer des tables
DATA: lt_act TYPE TABLE OF string,
lt_exp TYPE TABLE OF string.
lt_act = VALUE #( ( `A` ) ( `B` ) ).
lt_exp = VALUE #( ( `A` ) ( `B` ) ).
cl_abap_unit_assert=>assert_equals(
act = lt_act
exp = lt_exp
).
ENDMETHOD.
METHOD test_assert_true_false.
DATA: lv_flag TYPE abap_bool VALUE abap_true.
" Vérifier si vrai
cl_abap_unit_assert=>assert_true(
act = lv_flag
msg = 'Le flag devrait être vrai"
).
" Vérifier si faux
cl_abap_unit_assert=>assert_false(
act = xsdbool( 1 = 2 )
msg = 'L''expression devrait être fausse"
).
ENDMETHOD.
METHOD test_assert_initial.
DATA: lv_empty TYPE string,
lv_filled TYPE string VALUE 'Test'.
" Vérifier si initial (vide)
cl_abap_unit_assert=>assert_initial(
act = lv_empty
msg = 'La variable devrait être initiale"
).
" Vérifier si NON initial
cl_abap_unit_assert=>assert_not_initial(
act = lv_filled
msg = 'La variable ne devrait pas être initiale"
).
ENDMETHOD.
METHOD test_assert_bound.
DATA: lo_object TYPE REF TO lcl_calculator,
lo_null TYPE REF TO lcl_calculator.
lo_object = NEW #( ).
" Vérifier si la référence est liée
cl_abap_unit_assert=>assert_bound(
act = lo_object
msg = 'La référence d''objet devrait être liée"
).
" Vérifier si NON liée
cl_abap_unit_assert=>assert_not_bound(
act = lo_null
msg = 'La référence d''objet devrait être NULL"
).
ENDMETHOD.
METHOD test_assert_differs.
" Vérifier que les valeurs sont différentes
cl_abap_unit_assert=>assert_differs(
act = 'ABC"
exp = 'XYZ"
msg = 'Les valeurs devraient être différentes"
).
ENDMETHOD.
METHOD test_assert_char_cp.
" Pattern-Matching (comme CP dans IF)
cl_abap_unit_assert=>assert_char_cp(
act = 'Hello World"
exp = 'Hello*"
msg = 'La chaîne devrait commencer par Hello"
).
ENDMETHOD.
METHOD test_fail.
" Faire échouer le test intentionnellement
IF 1 = 2.
cl_abap_unit_assert=>fail(
msg = 'Ce code ne devrait jamais être atteint"
).
ENDIF.
ENDMETHOD.
ENDCLASS.

3. Setup et Teardown

CLASS ltc_lifecycle DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
CLASS-DATA: gv_class_setup_done TYPE abap_bool.
DATA: mv_setup_done TYPE abap_bool.
CLASS-METHODS:
class_setup, " Une fois avant tous les tests
class_teardown. " Une fois après tous les tests
METHODS:
setup, " Avant chaque test
teardown, " Après chaque test
test_first FOR TESTING,
test_second FOR TESTING.
ENDCLASS.
CLASS ltc_lifecycle IMPLEMENTATION.
METHOD class_setup.
" Initialisation unique pour tous les tests
" ex. Créer des données de test en DB
gv_class_setup_done = abap_true.
ENDMETHOD.
METHOD class_teardown.
" Nettoyage après tous les tests
" ex. Supprimer les données de test de la DB
ENDMETHOD.
METHOD setup.
" Avant CHAQUE cas de test
" ex. Réinstancier l'objet
mv_setup_done = abap_true.
ENDMETHOD.
METHOD teardown.
" Après CHAQUE cas de test
" ex. Réinitialiser les variables
CLEAR mv_setup_done.
ENDMETHOD.
METHOD test_first.
cl_abap_unit_assert=>assert_true( gv_class_setup_done ).
cl_abap_unit_assert=>assert_true( mv_setup_done ).
ENDMETHOD.
METHOD test_second.
cl_abap_unit_assert=>assert_true( gv_class_setup_done ).
cl_abap_unit_assert=>assert_true( mv_setup_done ).
ENDMETHOD.
ENDCLASS.

4. Tests d’exceptions

CLASS lcl_validator DEFINITION.
PUBLIC SECTION.
METHODS: validate_age IMPORTING iv_age TYPE i
RAISING cx_parameter_invalid.
ENDCLASS.
CLASS lcl_validator IMPLEMENTATION.
METHOD validate_age.
IF iv_age < 0.
RAISE EXCEPTION TYPE cx_parameter_invalid
EXPORTING parameter = 'AGE'.
ENDIF.
ENDMETHOD.
ENDCLASS.
CLASS ltc_validator DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA: mo_cut TYPE REF TO lcl_validator.
METHODS:
setup,
test_valid_age FOR TESTING,
test_negative_age_raises FOR TESTING.
ENDCLASS.
CLASS ltc_validator IMPLEMENTATION.
METHOD setup.
mo_cut = NEW #( ).
ENDMETHOD.
METHOD test_valid_age.
" Ne devrait pas lever d'exception
TRY.
mo_cut->validate_age( 25 ).
CATCH cx_parameter_invalid.
cl_abap_unit_assert=>fail( 'Exception inattendue' ).
ENDTRY.
ENDMETHOD.
METHOD test_negative_age_raises.
" Exception attendue
TRY.
mo_cut->validate_age( -5 ).
cl_abap_unit_assert=>fail( 'Exception attendue' ).
CATCH cx_parameter_invalid INTO DATA(lx_error).
" Exception attendue - test réussi
cl_abap_unit_assert=>assert_equals(
act = lx_error->parameter
exp = 'AGE"
).
ENDTRY.
ENDMETHOD.
ENDCLASS.

5. Test Doubles (Mocking)

" Interface pour l'injection de dépendances
INTERFACE lif_database.
METHODS: get_customer IMPORTING iv_id TYPE i
RETURNING VALUE(rs_customer) TYPE ty_customer.
ENDINTERFACE.
" Implémentation de production
CLASS lcl_database DEFINITION.
PUBLIC SECTION.
INTERFACES: lif_database.
ENDCLASS.
" Classe à tester
CLASS lcl_customer_service DEFINITION.
PUBLIC SECTION.
METHODS: constructor IMPORTING io_db TYPE REF TO lif_database.
METHODS: get_customer_name IMPORTING iv_id TYPE i
RETURNING VALUE(rv_name) TYPE string.
PRIVATE SECTION.
DATA: mo_db TYPE REF TO lif_database.
ENDCLASS.
CLASS lcl_customer_service IMPLEMENTATION.
METHOD constructor.
mo_db = io_db.
ENDMETHOD.
METHOD get_customer_name.
DATA(ls_customer) = mo_db->get_customer( iv_id ).
rv_name = ls_customer-name.
ENDMETHOD.
ENDCLASS.
" Test Double (Mock)
CLASS ltd_database DEFINITION FOR TESTING.
PUBLIC SECTION.
INTERFACES: lif_database.
DATA: ms_mock_customer TYPE ty_customer.
ENDCLASS.
CLASS ltd_database IMPLEMENTATION.
METHOD lif_database~get_customer.
" Retourne des données mock au lieu d'un accès DB
rs_customer = ms_mock_customer.
ENDMETHOD.
ENDCLASS.
" Classe de test avec Mock
CLASS ltc_customer_service DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA: mo_cut TYPE REF TO lcl_customer_service,
mo_mock_db TYPE REF TO ltd_database.
METHODS:
setup,
test_get_customer_name FOR TESTING.
ENDCLASS.
CLASS ltc_customer_service IMPLEMENTATION.
METHOD setup.
" Créer le mock
mo_mock_db = NEW #( ).
mo_mock_db->ms_mock_customer = VALUE #(
id = 1
name = 'Client Test"
).
" Injecter le mock dans la CUT
mo_cut = NEW #( io_db = mo_mock_db ).
ENDMETHOD.
METHOD test_get_customer_name.
DATA(lv_name) = mo_cut->get_customer_name( 1 ).
cl_abap_unit_assert=>assert_equals(
act = lv_name
exp = 'Client Test"
).
ENDMETHOD.
ENDCLASS.

6. Attributs de test

CLASS ltc_attributes DEFINITION FOR TESTING
DURATION MEDIUM " SHORT | MEDIUM | LONG
RISK LEVEL DANGEROUS. " HARMLESS | DANGEROUS | CRITICAL
PRIVATE SECTION.
METHODS:
" Catégorisation
test_quick FOR TESTING,
" Ignorer le test
test_not_yet_implemented FOR TESTING.
ENDCLASS.
CLASS ltc_attributes IMPLEMENTATION.
METHOD test_quick.
" Test normal
cl_abap_unit_assert=>assert_true( abap_true ).
ENDMETHOD.
METHOD test_not_yet_implemented.
" Marquer le test comme "pas encore implémenté"
cl_abap_unit_assert=>skip( 'Pas encore implémenté' ).
ENDMETHOD.
ENDCLASS.

7. Données de test avec méthodes helper

CLASS ltc_with_helpers DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS:
test_process_orders FOR TESTING,
" Méthodes helper (pas FOR TESTING)
given_orders RETURNING VALUE(rt_orders) TYPE ty_orders,
given_customer RETURNING VALUE(rs_customer) TYPE ty_customer.
ENDCLASS.
CLASS ltc_with_helpers IMPLEMENTATION.
METHOD test_process_orders.
" Given
DATA(lt_orders) = given_orders( ).
DATA(ls_customer) = given_customer( ).
" When
DATA(lv_result) = process( lt_orders ).
" Then
cl_abap_unit_assert=>assert_equals(
act = lv_result
exp = 'OK"
).
ENDMETHOD.
METHOD given_orders.
rt_orders = VALUE #(
( id = 1 amount = 100 )
( id = 2 amount = 200 )
).
ENDMETHOD.
METHOD given_customer.
rs_customer = VALUE #( id = 1 name = 'Test' ).
ENDMETHOD.
ENDCLASS.

8. Tests de méthodes privées

" Classe à tester
CLASS lcl_processor DEFINITION
FRIENDS ltc_processor. " Déclarer la classe de test comme friend
PUBLIC SECTION.
METHODS: process RETURNING VALUE(rv_result) TYPE string.
PRIVATE SECTION.
METHODS: calculate_internal RETURNING VALUE(rv_value) TYPE i.
ENDCLASS.
" La classe de test peut accéder aux méthodes privées
CLASS ltc_processor DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA: mo_cut TYPE REF TO lcl_processor.
METHODS:
setup,
test_calculate_internal FOR TESTING.
ENDCLASS.
CLASS ltc_processor IMPLEMENTATION.
METHOD setup.
mo_cut = NEW #( ).
ENDMETHOD.
METHOD test_calculate_internal.
" Accès direct à la méthode privée grâce à FRIENDS
DATA(lv_value) = mo_cut->calculate_internal( ).
cl_abap_unit_assert=>assert_equals(
act = lv_value
exp = 42
).
ENDMETHOD.
ENDCLASS.

9. SQL Test Double Framework

CLASS ltc_with_sql_double DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
CLASS-DATA: mo_environment TYPE REF TO if_osql_test_environment.
CLASS-METHODS:
class_setup,
class_teardown.
METHODS:
setup,
test_read_customer FOR TESTING.
ENDCLASS.
CLASS ltc_with_sql_double IMPLEMENTATION.
METHOD class_setup.
" Créer l'environnement de test pour les tables DB
mo_environment = cl_osql_test_environment=>create(
i_dependency_list = VALUE #( ( 'KNA1' ) )
).
ENDMETHOD.
METHOD class_teardown.
mo_environment->destroy( ).
ENDMETHOD.
METHOD setup.
" Insérer les données de test
DATA: lt_kna1 TYPE TABLE OF kna1.
lt_kna1 = VALUE #(
( mandt = sy-mandt kunnr = '0000001000' name1 = 'Client Test' )
).
mo_environment->insert_test_data( lt_kna1 ).
ENDMETHOD.
METHOD test_read_customer.
" Le SELECT lit maintenant depuis le Test Double au lieu de la vraie DB
SELECT SINGLE name1 FROM kna1
WHERE kunnr = '0000001000"
INTO @DATA(lv_name).
cl_abap_unit_assert=>assert_equals(
act = lv_name
exp = 'Client Test"
).
ENDMETHOD.
ENDCLASS.

Exécution des tests

" Dans Eclipse/ADT :
CTRL + SHIFT + F10 " Exécuter tous les tests
Clic droit → Run As → ABAP Unit Test
" Dans SE80 :
Clic droit → Exécuter le test unitaire

DURATION et RISK LEVEL

DURATIONDurée d’exécution attendue
SHORT< 1 seconde
MEDIUM< 5 secondes
LONG> 5 secondes
RISK LEVELDescription
HARMLESSAucune modification de base de données
DANGEROUSModifications possibles des données de test
CRITICALLes données de production pourraient être affectées

Conseils importants / Bonnes pratiques

  • Classes de test dans les classes locales (définition/implémentation à la fin de la classe).
  • Nommage : ltc_ pour les classes de test, ltd_ pour les Test Doubles, test_ pour les méthodes.
  • Pattern AAA : Arrange (Given), Act (When), Assert (Then).
  • Une assertion par test - des tests focalisés sont plus faciles à maintenir.
  • Setup/Teardown pour l’initialisation/nettoyage répété.
  • Injection de dépendances pour une architecture testable avec des mocks.
  • FRIENDS permet de tester les méthodes privées (à utiliser avec parcimonie).
  • cl_abap_unit_assert=>skip() pour les tests pas encore implémentés.
  • SQL Test Double Framework pour des tests indépendants de la base de données.
  • Exécuter les tests régulièrement - idéalement dans le pipeline CI/CD.