Эта статья ориентирована на ABAP-разработчиков в системах SAP ERP. Она содержит много специфических для платформы моментов, которые малоинтересны или даже спорны для разработчиков, использующих другие платформы.
Это вторая часть публикации. Начало можно прочитать тут: Модульные тесты в ABAP. Часть первая. Первый тест
Первый шаг сделан. Теперь нужно расширить и углубить наше наступление. Глобальная цель – максимально полное покрытие тестами, в рамках целесообразности происходящего. Под пристальным наблюдением — экзиты.
Под катом я приведу несколько примеров граблей, на которые можно наступить.
Допустим, наш ФМ делает не замещение значений, а проверку:
Тут есть две проблемы.
Во-первых, если попробовать делать прямой вызов:
То обнаружится, что прогон теста падает с не очень внятным сообщением:
Можно было рассудить, что раз упало, следовательно была ошибка, и значит всё хорошо. Но это не так, потому что тест должен быть зелененьким, а не красненьким.
Если это было бы настоящее исключение, то можно было бы заключить вызов в конструкцию TRY-CATCH и проверить, действительно ли ловится исключение:
В данном случае исключение происходит в самом движке ABAP Unit, а не в тестирующем или тестируемом коде. Следовательно, необходимо ловить его другим способом.
Сторонний наблюдатель, который не понимает внутреннюю кухню ABAP, мог бы заявить, что в таком случае необходимо отрефакторить сам функциональный модуль таким образом, чтобы он прямо возвращал ошибку, а не так чтобы эта ошибка бумкнула внутри него.
Это не так, и на это есть причины:
И вот оказывается, что у конструкции CALL FUNCTION есть дополнение:
Это дополнение предназначено именно для подобных случаев.
И вот мы теперь пишем тест таким образом:
Вот теперь тест проходит правильно.
Во-вторых, из-за того что ошибка нечёткая, то в данном случае мы не можем доказать, что произошла именно нужная нам ошибка. Внутри ФМ может быть запрятано сто двадцать пять разных ошибок на разные случаи жизни. У хорошей ошибки должны быть все необходимые атрибуты: тип, класс, номер, параметры.
Значит нужно немного нарефакторить сам ФМ, причём такой рефакторинг пойдёт ему на пользу.
Было:
Стало:
BTW: Вот это называется “ошибка повышенной чёткости”.
И после этого мы можем дополнить наш тест проверкой:
BTW: в стандартной библиотеке есть много разных уточняющих смысл вариаций метода ASSERT, не видно методов, чтобы подсластить именно такую пачку. Впрочем, можно замутить свой ASSERT, с сахаром и гитхабом.
Есть у меня для примера экзит EXIT_SAPMF02K_001.
Вот только незадача. Все экзиты CMOD устроены следующими образом: есть стандартная группа функций XF05, в которой есть функциональный модуль EXIT_SAPMF02K_001, в котором есть только строка INCLUDE ZXF05U01, уже в этом инклюде написан весь нужный код.
Вот и вопрос: на что создавать модульный тест?
Его нельзя создать на стандартную группу функций, потому что для этого потребуется её модифицировать, что не есть comme il faut.
Есть варианты.
Можно сделать копии функциональных модулей, так как внутри ФМ только одна строка кода, которая никогда не поменяется. После этого модульные тесты можно писать на эти Z-функции. Этот вариант прост и прям, поэтому и предпочтителен.
Все остальные варианты менее прямы, поэтому менее предпочтительны. Модульные тесты – это не то место, где стоит хитрить без повода.
Внутри экзита могут производиться запросы к БД, например:
Такие вещи всегда считались спорными для модульного тестирования. Однако жить как-то надо.
Делать запросы к БД – законное желание разработчика, тем более раз уж стандарт не предоставляет достаточно информации в своём интерфейсе.
Один из способов: переключить соответствующие локальные переменные/структуры на опциональные параметры экзита или глобальные переменные в группе функций. Соответственно в момент теста нужно будет заполнить и их. К сожалению, тут потребуется внести изменения в продуктивный код. Например:
Вариант выглядит не очень красиво, опциональный параметр (IMPORTING, CHAHGING или даже TABLES) выглядел бы немного лучше.
Но есть пара противопоказаний:
Можно рассмотреть ещё вариант с предварительным заполнением БД необходимыми для теста данными. В некоторых сценариях это имеет смысл: например, если для проводки документа необходимо проверять кредитора на резидентство, то можно просто подсунуть и настоящего кредитора, а не заниматься его симуляцией.
Изредка бывает, что внутри экзита нет каких-либо дополнительных атрибутов передаваемого объекта. И чтобы заполучить их, мы используем хак с ASSIGN следующего вида:
И что же может модульное тестирование поделать с таким грубым отношением к области видимости? Ничего. По возможности избегайте этого.
Это серьёзный повод для раздумий.
Можно попытаться вырулить как в предыдущей грабле, можно попробовать найти более подходящий экзит, можно попытаться опереться на другие параметры, можно попробовать обеспечить передачу нужных параметров внутри заявленного интерфейса экзита… А можно оставить этот участок кода непокрытым… Пока 100% покрытие – не самоцель, а тестировать нужно сначала то, что может сломаться.
Кстати о “сломаться”. Недавно был случай, что после обновления в стандарте исходная переменная поменяла свой тип, поэтому код в экзите сломался с вытекающими последствиями.
Иногда в коде экзита можно встретить проверку на код транзакции:
В рамках нашей симуляции код транзакции является получается из окружения, а не из интерфейса самого экзита. Потому в модульном тесте SY-TCODE будет показывать транзакцию разработки SE37.
Варианты:
Ну и напоследок: если уж подрефакторить не удалось, то можно вполне и:
Будет работать, большого криминала тут не вижу.
На сегодня пока хватит. Снимаю защитный шлем и откланиваюсь. Спасибо за внимание.
Это вторая часть публикации. Начало можно прочитать тут: Модульные тесты в ABAP. Часть первая. Первый тест
Первый шаг сделан. Теперь нужно расширить и углубить наше наступление. Глобальная цель – максимально полное покрытие тестами, в рамках целесообразности происходящего. Под пристальным наблюдением — экзиты.
Под катом я приведу несколько примеров граблей, на которые можно наступить.
Грабля первая. Обработка ошибок.
Допустим, наш ФМ делает не замещение значений, а проверку:
function zfi_bte_00001120.
if ls_bseg-zuonr eq space.
message ‘Поле Присвоение обязательно для заполнения’ type ‘E’.
endif.
endfunction.
Тут есть две проблемы.
Во-первых, если попробовать делать прямой вызов:
call function 'ZFI_BTE_00001120'
tables
t_bkpf = t_bkpf
t_bseg = t_bseg
t_bkpfsub = t_bkpfsub
t_bsegsub = t_bsegsub.
То обнаружится, что прогон теста падает с не очень внятным сообщением:
Exception Error <CX_AUNIT_UNCAUGHT_MESSAGE>
Можно было рассудить, что раз упало, следовательно была ошибка, и значит всё хорошо. Но это не так, потому что тест должен быть зелененьким, а не красненьким.
Если это было бы настоящее исключение, то можно было бы заключить вызов в конструкцию TRY-CATCH и проверить, действительно ли ловится исключение:
try.
call function 'ZFI_BTE_00001120'.
catch CX_AUNIT_UNCAUGHT_MESSAGE.
lv_catched = 'X'.
endtry
cl_abap_unit_assert=>assert_true( lv_catched ).
В данном случае исключение происходит в самом движке ABAP Unit, а не в тестирующем или тестируемом коде. Следовательно, необходимо ловить его другим способом.
Сторонний наблюдатель, который не понимает внутреннюю кухню ABAP, мог бы заявить, что в таком случае необходимо отрефакторить сам функциональный модуль таким образом, чтобы он прямо возвращал ошибку, а не так чтобы эта ошибка бумкнула внутри него.
Это не так, и на это есть причины:
- Мы не можем как-либо поменять интерфейс этого ФМ, потому что вызываем его не мы. И мы не можем исправить место его вызова, потому что это значит “ломать стандарт”. Такая особенность у экзитов.
- Не следует вводить в ФМ технические опциональные параметры в стиле THIS_IS_TEST и TEST_RESULT, а потом это внутри ФМ делать различные действия, исходя из этих параметров. Такой костыль своё дело сделает, но очень вредно засорять продуктивный код действиями, которые нужны только для теста.
И вот оказывается, что у конструкции CALL FUNCTION есть дополнение:
… EXCEPTIONS … error_message = n_error …
Это дополнение предназначено именно для подобных случаев.
И вот мы теперь пишем тест таким образом:
call function 'ZFI_BTE_00001120'
tables
t_bkpf = t_bkpf
t_bseg = t_bseg
t_bkpfsub = t_bkpfsub
t_bsegsub = t_bsegsub.
exceptions
error_message = 99.
cl_aunit_assert=>assert_subrc( act = sy-subrc exp = 99 ).
Вот теперь тест проходит правильно.
Во-вторых, из-за того что ошибка нечёткая, то в данном случае мы не можем доказать, что произошла именно нужная нам ошибка. Внутри ФМ может быть запрятано сто двадцать пять разных ошибок на разные случаи жизни. У хорошей ошибки должны быть все необходимые атрибуты: тип, класс, номер, параметры.
Значит нужно немного нарефакторить сам ФМ, причём такой рефакторинг пойдёт ему на пользу.
Было:
message ‘Поле Присвоение обязательно для заполнения’ type ‘E’.
Стало:
message e001(zfi_subst). "Поле Присвоение обязательно для заполнения
BTW: Вот это называется “ошибка повышенной чёткости”.
И после этого мы можем дополнить наш тест проверкой:
cl_aunit_assert=>assert_equals( act = sy-msgty exp = 'E' ).
cl_aunit_assert=>assert_equals( act = sy-msgid exp = 'ZFI_SUBST' ).
cl_aunit_assert=>assert_equals( act = sy-msgno exp = '001' ).
BTW: в стандартной библиотеке есть много разных уточняющих смысл вариаций метода ASSERT, не видно методов, чтобы подсластить именно такую пачку. Впрочем, можно замутить свой ASSERT, с сахаром и гитхабом.
Грабля вторая: CMOD
Есть у меня для примера экзит EXIT_SAPMF02K_001.
Вот только незадача. Все экзиты CMOD устроены следующими образом: есть стандартная группа функций XF05, в которой есть функциональный модуль EXIT_SAPMF02K_001, в котором есть только строка INCLUDE ZXF05U01, уже в этом инклюде написан весь нужный код.
Вот и вопрос: на что создавать модульный тест?
Его нельзя создать на стандартную группу функций, потому что для этого потребуется её модифицировать, что не есть comme il faut.
Есть варианты.
Можно сделать копии функциональных модулей, так как внутри ФМ только одна строка кода, которая никогда не поменяется. После этого модульные тесты можно писать на эти Z-функции. Этот вариант прост и прям, поэтому и предпочтителен.
Все остальные варианты менее прямы, поэтому менее предпочтительны. Модульные тесты – это не то место, где стоит хитрить без повода.
Грабля третья: Доступ к БД
Внутри экзита могут производиться запросы к БД, например:
if ls_bkpf-awtyp = 'TRAVL' and ls_bkpf-xblnr ne space.
select single * from bkpf into ls_bkpf_st
where bukrs = … and xblnr = ls_bkpf-xblnr and awtyp = 'TRAVL'.
if sy-subrc = 0.
...
endif.
endif.
Такие вещи всегда считались спорными для модульного тестирования. Однако жить как-то надо.
Делать запросы к БД – законное желание разработчика, тем более раз уж стандарт не предоставляет достаточно информации в своём интерфейсе.
Один из способов: переключить соответствующие локальные переменные/структуры на опциональные параметры экзита или глобальные переменные в группе функций. Соответственно в момент теста нужно будет заполнить и их. К сожалению, тут потребуется внести изменения в продуктивный код. Например:
if gs_bkpf_st is initial.
select single * from bkpf into ls_bkpf_st…
else.
ls_bkpf_st = gs_bkpf_st.
endif.
if ls_bkpf_st is not initial.
…
endif.
Вариант выглядит не очень красиво, опциональный параметр (IMPORTING, CHAHGING или даже TABLES) выглядел бы немного лучше.
if p_bkpf_st is supplied.
ls_bkpf_st = p_bkpf_st.
else.
select single * from bkpf into ls_bkpf_st…
endif.
if ls_bkpf_st is not initial.
…
endif.
Но есть пара противопоказаний:
- интерфейс будет отличаться от стандарта
- большое количество запросов к БД будет раздувать интерфейс ФМ
Можно рассмотреть ещё вариант с предварительным заполнением БД необходимыми для теста данными. В некоторых сценариях это имеет смысл: например, если для проводки документа необходимо проверять кредитора на резидентство, то можно просто подсунуть и настоящего кредитора, а не заниматься его симуляцией.
Грабля четвёртая: ASSIGN наверх
Изредка бывает, что внутри экзита нет каких-либо дополнительных атрибутов передаваемого объекта. И чтобы заполучить их, мы используем хак с ASSIGN следующего вида:
assign ('(SAPMF05A)UF05A-STGRD') to <stgrd>.
if sy-subrc = 0.
if <stgrd> = '02'.
…
endif.
endif.
И что же может модульное тестирование поделать с таким грубым отношением к области видимости? Ничего. По возможности избегайте этого.
Это серьёзный повод для раздумий.
Можно попытаться вырулить как в предыдущей грабле, можно попробовать найти более подходящий экзит, можно попытаться опереться на другие параметры, можно попробовать обеспечить передачу нужных параметров внутри заявленного интерфейса экзита… А можно оставить этот участок кода непокрытым… Пока 100% покрытие – не самоцель, а тестировать нужно сначала то, что может сломаться.
Кстати о “сломаться”. Недавно был случай, что после обновления в стандарте исходная переменная поменяла свой тип, поэтому код в экзите сломался с вытекающими последствиями.
Грабля пятая: Проверка на код транзакции
Иногда в коде экзита можно встретить проверку на код транзакции:
if ( sy-tcode = 'ASKBN' or sy-tcode = 'ASKB' ) and ls_bkpf-blart = 'AC'.
…
endif.
В рамках нашей симуляции код транзакции является получается из окружения, а не из интерфейса самого экзита. Потому в модульном тесте SY-TCODE будет показывать транзакцию разработки SE37.
Варианты:
- В первую очередь: если подумать, то в некоторых случаях проверку транзакции можно опустить. Часто она бывает избыточной.
- Во вторую очередь: если подумать, то в некоторых случаях можно определить транзакцию исходя из других атрибутов. Например, в случае того же экзита BTE 1120 есть признак BKPF-AWTYP.
Ну и напоследок: если уж подрефакторить не удалось, то можно вполне и:
sy-tcode = 'FB01'.
Будет работать, большого криминала тут не вижу.
На сегодня пока хватит. Снимаю защитный шлем и откланиваюсь. Спасибо за внимание.
amok
Большинство описанных граблей решается грамотной реализацией внутри ФМ'ов и EXIT'ов. Например, чтобы избежать исключений связанных с отсутствием обработки ошибки из ФМ'a, реализацию ФМ'a можно обернуть в ООП стиле, тогда ФМ сам по себе будет служить исключительно обёрткой над ООП моделью. В юнит-тестах будут тестироваться не ФМ, а ООП модели.
Жестко зашитые выборки в ABAP 7.5. можно обойти через тестовые инъекции. Хотя как мне кажется, лучше все таки программировать заранее обдумывая моменты тестирования, чем потом выкручиваться за счёт таких инъекций.
ivanbolhovitinov
Конечно, чем грамотнее код, тем проще на него писать тесты, кто бы спорил.
Мне не кажется, что дополнительные обёртки улучшат продуктивный код. Только лишние наслоения. Как говорили умные люди: любую проблему можно лишить путём введения дополнительного уровня абстракции, кроме проблемы слишком большого количества уровней абстракций. Или вы поясните на примере?
Вот у меня тут наоборотный пример под рукой есть. Есть БАДИ, значит типа ООП. Захожу, смотрю. Каждый метод представлен в виде ФМ из одной общей группы. И в этом обёрточном ООП нет ничего кроме вызова соответствующих ФМ с передачей всех параметров. Грабля это или нет, я пока не определился. Пока работает, ну и ладно, чего трогать-то.
А насчет 7.50 это пока не ко мне, у нас только с полгода как сделали апгрейд с 7.0 на 7.4, хватит пока апгрейдов. Вот доживём, тогда и пощупаем.