Cтолкнулся с новым фреймворком Test Double. В чём же основной смысл и назначение этого фреймворка, с чем его едят?
Дано: Основной класс ZCL_ACTION
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
CLASS zcl_action DEFINITION. PUBLIC SECTION. METHODS constructor IMPORTING actor TYPE REF TO zcl_actor. METHODS who_am_i RETURNING VALUE(rv_name) TYPE string. PRIVATE SECTION. DATA: mo_actor TYPE REF TO zcl_actor. ENDCLASS. CLASS zcl_action IMPLEMENTATION. METHOD constructor. mo_actor = actor. ENDMETHOD. METHOD who_am_i. rv_name = mo_actor->get_name( ). ENDMETHOD. ENDCLASS. |
И реализация класса ZCL_ACTOR при этом следующая:
1 2 3 4 5 6 7 8 9 10 11 |
CLASS zcl_actor DEFINITION . PUBLIC SECTION. METHODS: get_name RETURNING VALUE(rv_name) TYPE string. ENDCLASS. CLASS zcl_actor IMPLEMENTATION. METHOD get_name. rv_name = 'Real value'. ENDMETHOD. ENDCLASS. |
Требуется: написать юнит-тест для класса ZCL_ACTION.
Можно конечно написать так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
CLASS lcl_action_test DEFINITION FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. PUBLIC SECTION. METHODS who_am_i FOR TESTING. ENDCLASS. CLASS lcl_action_test IMPLEMENTATION. METHOD who_am_i. DATA(lo_action) = NEW zcl_action( NEW zcl_actor( ) ). DATA(lv_return) = lo_action->who_am_i( ). cl_abap_unit_assert=>assert_equals( act = lv_return exp = 'Real value' ). ENDMETHOD. ENDCLASS. |
Но здесь всё не совсем честно. Каждый юнит-тест должен проверять свой отдельный класс и быть условно-независимым от других классов. Тем более, что зависмый класс может находиться в то же самое время в разработке у другого разработчика или содержать громоздкую инициализацию.
По стандартным правилам мы должны написать mock-объект для класса ZCL_ACTOR и в тестах ZCL_ACTION использовать именно его. И так как такое занятие может быть несколько утомительным, то при помощи фреймворка Test Double мы можем на лету создать mock-объект и подсунуть его в наш тестируемый класс. Именно в этом и заключается профит.
Необходимые требования:
- для зависимого класса должен быть сделан глобальный интерфейс, обращение к зависимому классу должно быть сделано через этот интерфейс
- количество использований за один раз несколько ограничено, так как данный фреймворк временно генерирует настоящий ABAP-класс, представляющий требуемый интерфейс (GENERATE SUBROUTINE POOL со всеми вытекающими)
- поддерживается только в версиях 7.40/7.50 и выше
- ну наверняка чего-то не умеет по мелочи (статические методы, исключения не-class-based)
Сначала разберёмся со вспомогательным классом, вынесем наружный интерфейс:
1 2 3 4 5 6 7 8 9 10 11 |
CLASS zcl_actor DEFINITION . PUBLIC SECTION. INTERFACES: zif_actor. ENDCLASS. CLASS zcl_actor IMPLEMENTATION. METHOD zif_actor~get_name. rv_name = 'Real value'. ENDMETHOD. ENDCLASS. |
Приспособим интерфейс внутри основного класса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
CLASS zcl_action DEFINITION. PUBLIC SECTION. METHODS constructor IMPORTING actor TYPE REF TO zif_actor. METHODS who_am_i RETURNING VALUE(rv_name) TYPE string. PRIVATE SECTION. DATA: mo_actor TYPE REF TO zif_actor. ENDCLASS. CLASS zcl_action IMPLEMENTATION. METHOD constructor. mo_actor = actor. ENDMETHOD. METHOD who_am_i. rv_name = mo_actor->get_name( ). ENDMETHOD. ENDCLASS. |
И hello-world в нашем случае будет таким:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
CLASS lcl_action_test DEFINITION FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. PUBLIC SECTION. METHODS who_am_i FOR TESTING. ENDCLASS. CLASS lcl_action_test IMPLEMENTATION. METHOD who_am_i. DATA: lo_actor TYPE REF TO zif_actor. lo_actor ?= cl_abap_testdouble=>create( 'zif_actor' ). cl_abap_testdouble=>configure_call( lo_actor ) ->returning( 'Fake value' ). lo_actor->get_name( ). DATA(lo_action) = NEW zcl_action( lo_actor ). DATA(lv_return) = lo_action->who_am_i( ). cl_abap_unit_assert=>assert_equals( act = lv_return exp = 'Fake value' ). ENDMETHOD. ENDCLASS. |
Разберём магию:
Шаг 1:
lo_actor ?= cl_abap_testdouble=>create( ‘zif_actor’ )
Создаёт и возвращает mock-объект по требуемому интерфейсу.
Шаг 2:
cl_abap_testdouble=>configure_call( lo_actor ) ->returning( ‘Fake value’ )
Задаёт конфигурацию, подготавливает выходные параметры для ожидаемого вызова.
Шаг 3:
lo_actor->get_name( )
Инициализирует метод, который требуется замокать на данную конфигурацию.
И дальше уже можно спокойно использовать этот псевдо-объект в тестах, когда дойдёт дело до нужного метода, то заданное значение всплывёт из конфигурации.