Эта статья ориентирована на 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. Часть третья. Всяческая суета
Это вторая часть публикации. Начало можно прочитать тут: Модульные тесты в 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'.
Будет работать, большого криминала тут не вижу.
На сегодня пока хватит. Снимаю защитный шлем и откланиваюсь. Спасибо за внимание.
Продолжение можно почитать тут: Модульные тесты в ABAP. Часть третья. Всяческая суета
