Перейти до вмісту

Injection-патерни для тестування

Щоб написати unit-тест на клас з залежностями, треба підсунути замість реальної залежності фальшивку (fake, stub, mock). У ABAP є кілька способів це зробити — від найчистішого (constructor injection) до аварійного (backdoor через FRIENDS).

Ця сторінка — шпаргалка, коли який спосіб застосовувати.

1. Constructor Injection — стандарт

Section titled “1. Constructor Injection — стандарт”

Залежність передається як параметр конструктора. Це перший вибір. Інші способи — коли цей не працює.

CLASS zcl_order_service DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
constructor
IMPORTING io_dao TYPE REF TO zif_order_dao,
place
IMPORTING iv_customer TYPE kunnr
RAISING zcx_order_failed.
PRIVATE SECTION.
DATA mo_dao TYPE REF TO zif_order_dao.
ENDCLASS.

У production:

DATA(lo_svc) = NEW zcl_order_service( NEW zcl_order_dao_db( ) ).

У тесті:

CLASS ltc_order_service DEFINITION FOR TESTING DURATION SHORT RISK LEVEL HARMLESS
FINAL.
PRIVATE SECTION.
METHODS test_place FOR TESTING.
ENDCLASS.
CLASS ltc_order_service IMPLEMENTATION.
METHOD test_place.
DATA(lo_fake_dao) = NEW zcl_order_dao_in_memory( ).
DATA(lo_sut) = NEW zcl_order_service( lo_fake_dao ).
lo_sut->place( iv_customer = '4711' ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lo_fake_dao->get_insert_count( ) ).
ENDMETHOD.
ENDCLASS.

Переваги:

  • Залежність видно у сигнатурі — зрозуміло без читання коду.
  • Обʼєкт інваріантно валідний з моменту створення.
  • Компілятор перевіряє, що залежність є.

Недоліки:

  • Якщо залежність опційна — конструктор роздувається IMPORTING ... OPTIONAL.
  • Для існуючого класу без потрібного параметра — доведеться правити всіх клієнтів.

2. Setter Injection — коли залежність опційна чи змінна

Section titled “2. Setter Injection — коли залежність опційна чи змінна”
METHODS set_dao IMPORTING io_dao TYPE REF TO zif_order_dao.

Коли:

  • Залежність опційна — працюватиме з дефолтом, або можна підмінити в тесті.
  • Залежність змінюється в runtime (перемикання фабрик у тестовому сценарії).
  • Клас успадковує SAP-стандартний клас, чий конструктор ми не контролюємо.

Недоліки:

  • Обʼєкт може бути у невалідному стані між NEW і set_*. Перевіряй IF mo_dao IS NOT BOUND у методах, що використовують.
  • Не очевидно з коду, що залежність треба задати.

3. Backdoor Injection — через LOCAL / GLOBAL FRIENDS

Section titled “3. Backdoor Injection — через LOCAL / GLOBAL FRIENDS”

Коли конструктор — NO ARGUMENTS, setter-а нема, а рефакторити клас не можна — тест-клас робиться friend і задає mo_dao напряму.

CLASS zcl_legacy_service DEFINITION PUBLIC FINAL CREATE PUBLIC
GLOBAL FRIENDS zcl_legacy_service_test. " або LOCAL FRIENDS ltc_test
PUBLIC SECTION.
METHODS do_something.
PRIVATE SECTION.
DATA mo_dao TYPE REF TO zif_order_dao.
METHODS constructor.
ENDCLASS.
CLASS zcl_legacy_service IMPLEMENTATION.
METHOD constructor.
mo_dao = NEW zcl_order_dao_db( ). " жорстко забито
ENDMETHOD.
ENDCLASS.

Тест:

CLASS ltc_test DEFINITION FOR TESTING FINAL.
PRIVATE SECTION.
METHODS:
setup,
test_something FOR TESTING.
DATA mo_sut TYPE REF TO zcl_legacy_service.
ENDCLASS.
CLASS ltc_test IMPLEMENTATION.
METHOD setup.
mo_sut = NEW zcl_legacy_service( ).
mo_sut->mo_dao = NEW zcl_order_dao_in_memory( ). " прямий доступ через friendship
ENDMETHOD.
ENDCLASS.

Недоліки:

  • GLOBAL FRIENDS ламає інкапсуляцію. Використовуй тільки для тестів.
  • Порядок NEW + set mo_dao крихкий — якщо конструктор вже зробив роботу з дефолтним DAO, пізніша підміна не допоможе.
  • Якщо LOCAL FRIENDS — тест-клас має жити в тому ж програмному обʼєкті, що перевіряється. Це обмежує.

Коли використовувати:

  • Legacy-клас, який не можна рефакторити.
  • Тимчасовий «милиця», доки команда не доведе клас до стану з DI.

4. Test Double через успадкування (REDEFINITION)

Section titled “4. Test Double через успадкування (REDEFINITION)”

Якщо треба замокати public-метод існуючого класу, і цей метод не FINAL:

CLASS ltc_fake_service DEFINITION INHERITING FROM zcl_order_service
FOR TESTING.
PUBLIC SECTION.
METHODS send REDEFINITION.
DATA mt_calls TYPE STANDARD TABLE OF string WITH EMPTY KEY.
ENDCLASS.
CLASS ltc_fake_service IMPLEMENTATION.
METHOD send.
APPEND iv_message TO mt_calls.
" не робимо реальну відправку
ENDMETHOD.
ENDCLASS.

У тесті підсовуєш NEW ltc_fake_service( ) замість реального сервісу. Потім перевіряєш lt_calls, що очікувані виклики були зроблені.

Недоліки:

  • Клас під тест має бути не FINAL, метод — не FINAL. У Clean ABAP FINAL — дефолт, тому цей шлях часто закритий.
  • Тест-клас — нащадок production-класу: будь-яка зміна production-ієрархії може зламати mock.

SAP з 7.52+ пропонує cl_abap_testdouble — generic mocking framework для інтерфейсів:

DATA(lo_mock_dao) = CAST zif_order_dao( cl_abap_testdouble=>create( 'zif_order_dao' ) ).
cl_abap_testdouble=>configure_call( lo_mock_dao )->returning( VALUE zty_order( ... ) ).
lo_mock_dao->get_by_id( '123' ).

Зручно для швидких моків без окремих fake-класів. Працює тільки з інтерфейсами (не з класами).

Недоліки:

  • Runtime-mocks — не завжди type-safe.
  • Помилки (неправильний mock) видно тільки при запуску тесту.
  • Потрібен 7.52+ ABAP release.
СитуаціяРекомендація
Новий клас, ти контролюєш дизайнConstructor injection
Залежність опційнаSetter injection (+ default у конструкторі)
Конструктор жорстко забитий, клас не рухомийBackdoor через FRIENDS — останній засіб
Треба замокати public-метод SAP-класуУспадкування + REDEFINITION (якщо не FINAL)
Мокинг інтерфейсу, хочеш експресивноcl_abap_testdouble
Моє — не тестуємо цю залежністьПропусти: тест вищого рівня або integration test
  • Перевикористання моків між тестами. Кожен тест — свіжа фальшивка. Інакше «забруднені» стани плавають між тестами.
  • Замість справжньої логіки — запис виклику. Фейк-DAO у тесті не повинен інтерпретувати бізнес-правила; він просто зберігає, що йому сказали. Бізнес-правила — у zcl_*_service, який ми тестуємо.
  • Надмірне моканням. Якщо у тесті 5 моків — можливо, клас має забагато залежностей. Один агрегуючий фасад міг би спростити.
  • CLASS-DATA і тест-ізоляція. Якщо клас під тест має CLASS-DATA (наприклад, singleton-пул flyweight) — тести впливають один на одного. teardown має явно очищати пул.