Эта статья ориентирована на ABAP-разработчиков в системах SAP ERP. Она содержит много специфических для платформы моментов, которые малоинтересны или даже спорны для разработчиков, использующих другие платформы.

Это вторая часть публикации. Начало можно прочитать тут: Модульные тесты в 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. Часть третья. Всяческая суета