Модульные тесты в ABAP. Часть вторая. Грабли

в 4:59, , рубрики: abap, ERP-системы, sap, tdd, разработка

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

Это вторая часть публикации. Начало можно прочитать тут: Модульные тесты в ABAP. Часть первая. Первый тест

Первый шаг сделан. Теперь нужно расширить и углубить наше наступление. Глобальная цель – максимально полное покрытие тестами, в рамках целесообразности происходящего. Под пристальным наблюдением — экзиты.

Модульные тесты в ABAP. Часть вторая. Грабли - 1

Под катом я приведу несколько примеров граблей, на которые можно наступить.

Грабля первая. Обработка ошибок.

Допустим, наш ФМ делает не замещение значений, а проверку:

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 initial. 
      …           
  endif.

Вариант выглядит стрёмно, опциональный параметр (CHANGING, TABLES) выглядел бы немного лучше. Но есть пара противопоказаний:

  • интерфейс будет отличаться от стандарта
  • большое количество запросов к БД будет раздувать интерфейс ФМ

Можно рассмотреть ещё вариант с предварительным заполнением БД необходимыми для теста данными. В некоторых сценариях это имеет смысл: например, если для проводки документа необходимо проверять кредитора на резидентство, то можно просто подсунуть и настоящего кредитора, а не заниматься его симуляцией.

Грабля четвёртая: 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.

Варианты:

  • В первую очередь: если подумать, то в некоторых случаях проверку транзакции можно опустить. Часто она бывает избыточной.
  • Во вторую очередь: если подумать, то в некоторых случаях можно определить транзакцию исходя из других атрибутов. Например, в случае того же экзита 1120 есть признак BKPF-AWTYP.

Ну и напоследок: если уж подрефакторить не удалось, то можно вполне и:

    sy-tcode = 'FB01'.

Будет работать, большого криминала тут не вижу.

На сегодня пока хватит. Снимаю защитный шлем и откланиваюсь. Спасибо за внимание.

Автор: ivanbolhovitinov

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js