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 ABAPFINAL— дефолт, тому цей шлях часто закритий. - Тест-клас — нащадок production-класу: будь-яка зміна production-ієрархії може зламати mock.
5. ABAP Test Double Framework (ATDF)
Section titled “5. ABAP Test Double Framework (ATDF)”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.
Вибір способу
Section titled “Вибір способу”| Ситуація | Рекомендація |
|---|---|
| Новий клас, ти контролюєш дизайн | Constructor injection |
| Залежність опційна | Setter injection (+ default у конструкторі) |
| Конструктор жорстко забитий, клас не рухомий | Backdoor через FRIENDS — останній засіб |
| Треба замокати public-метод SAP-класу | Успадкування + REDEFINITION (якщо не FINAL) |
| Мокинг інтерфейсу, хочеш експресивно | cl_abap_testdouble |
| Моє — не тестуємо цю залежність | Пропусти: тест вищого рівня або integration test |
Підводні камені
Section titled “Підводні камені”- Перевикористання моків між тестами. Кожен тест — свіжа фальшивка. Інакше «забруднені» стани плавають між тестами.
- Замість справжньої логіки — запис виклику. Фейк-DAO у тесті не повинен інтерпретувати бізнес-правила; він просто зберігає, що йому сказали. Бізнес-правила — у
zcl_*_service, який ми тестуємо. - Надмірне моканням. Якщо у тесті 5 моків — можливо, клас має забагато залежностей. Один агрегуючий фасад міг би спростити.
CLASS-DATAі тест-ізоляція. Якщо клас під тест маєCLASS-DATA(наприклад, singleton-пул flyweight) — тести впливають один на одного.teardownмає явно очищати пул.