Unit-тесты. История моих экспериментов
Более 10 лет назад я начал писать unit-тесты для ABAP. За эти годы я перепробовал различные подходы, узнал разные способы выполнения тестов и подготовки данных, разработал свои собственные библиотеки, которые помогали сделать тесты более универсальными и эффективными, наработал много практического опыта. Своим опытом я планирую поделиться в данной статье.
Вступление
Про unit-тесты я узнал впервые примерно тогда же, когда впервые узнал о scrum/agile. Я посетил семинар по методикам гибкой разработки в 2010 году, где говорилось про unit-тесты и подход TDD (test-drivendevelopment, разработка через тест). Я с удивлением обнаружил, что в SAP на тот момент уже существовал функционал AbapUnit, и с тех пор мои эксперименты начались.
Мой первый тест проверял очень сложный и запутанный код для самописного Z-приложения на WebDynpro, который в зависимости от полномочий пользователя и различных условий выводил или не выводил на панель инструментов те или иные кнопки.
При написании данного первого теста и нескольких других unit-тестов я совершил ошибку, которая очень усложнила мои тесты, сделала их долго работающими и ненадёжными и по сути превратила их не в unit-тесты, а в тесты какого-то другого вида – функциональные, интеграционные.
Как не надо было писать unit-тесты
Приведу два примера, как не нужно было делать.
В первом случае мне нужно было протестировать код по выводу различных кнопок различным пользователям в зависимости от полномочий, о котором я уже упоминал. Для этого я написал процедуру поиска пользователя с нужными полномочиями (их нужно было заранее завести в разработке, иначе тест падал) и во время прогона проверка authority-check выполнялась для определённого пользователя, а результат сравнивался с эталоном.
Во втором случае мне нужно было проверить код, который выполняет select в базу. Перед проверкой кода мне нужно было положить в таблицу определённые записи, в противном случае тест не работал.
Что же в перечисленных примерах было не так? В них отсутствовала изоляция теста от внешних вызовов – от базы данных, от наличия определённых записей в определённых таблицах на момент начала теста, от наличия тех или иных пользователей, от полномочий этих пользователей.
Что делать для исправления данной ситуации?
Изоляция unit-тестов
Простой пример.
Приведу на пальцах самый простой пример изоляции.
Есть метод, который делает select, затем к полученному значению прибавляет 1 и возвращает результат.
CLASS lcl_example_class DEFINITION.
  PUBLIC SECTION.
    METHODS get_value_plus_one
      IMPORTING iv_doc_id           TYPE char10
      RETURNING VALUE(return_value) TYPE i.
ENDCLASS.
CLASS lcl_example_class IMPLEMENTATION.
  METHOD get_value_plus_one.
    DATA lv_value TYPE i.
    SELECT SINGLE value
      INTO lv_value
      FROM ztable1
      WHERE doc_id = iv_doc_id.
return_value = lv_value + 1.
  ENDMETHOD.
ENDCLASS.
Первый вариант изоляции – добавить условие, при котором в тестовом режиме данные вместо базы данных будут считываться из тестового буфера.
CLASS lcl_example_class DEFINITION.
  PUBLIC SECTION.
    DATA test_mode TYPE abap_bool.
    DATA test_buffer TYPE TABLE OF ztable1.
    METHODS get_value_plus_one
      IMPORTING iv_doc_id           TYPE char10
      RETURNING VALUE(return_value) TYPE i.
ENDCLASS.
CLASS lcl_example_class IMPLEMENTATION.
  METHOD get_value_plus_one.
    DATA lv_value TYPE i.
    IF test_mode = abap_true.
      READ TABLE test_buffer WITH KEY doc_id = iv_doc_id
        ASSIGNING FIELD-SYMBOL(<ls_row>).
      IF sy-subrc = 0.
lv_value = <ls_row>-value.
      ENDIF.
    ELSE.
      SELECT SINGLE value
        INTO lv_value
        FROM ztable1
        WHERE doc_id = iv_doc_id.
    ENDIF.
return_value = lv_value + 1.
ENDMETHOD.
ENDCLASS.
Получается, что мы добавили индикатор тестового режима. Если режим тестовый, то данные читаются из заранее записанного буфера, а в обычном режиме делаем select. В этом случае принцип изоляции работает и цель достигнута, хотя код и не выглядит изящным.
Тест в этом случае может выглядеть следующим образом:
CLASS lcl_example_class_unit DEFINITION FOR TESTING RISK LEVEL HARMLESS.
  PUBLIC SECTION.
    METHODS run_test FOR TESTING.
ENDCLASS.
CLASS lcl_example_class_unit IMPLEMENTATION.
  METHOD run_test.
DATA(lo_cut) = new lcl_example_class( ).
lo_cut->test_mode = abap_true.
lO_cut->test_buffer = VALUE #( (doc_id = '0000000005' value = 10 ) ).
DATA(lv_result) = lo_cut->get_value_plus_one( '0000000005' ).
cl_aunit_assert=>assert_equals( act = lv_result
exp = 11 ).
  ENDMETHOD.
ENDCLASS.
Как можно улучшить код метода при условии сохранении изоляции? В ООП есть ответ на данный вопрос – внедрение зависимостей или DependencyInjection.
Внедрение зависимостей (DependencyInjection)
Сделаем отдельный интерфейс lif_table_reader, в нём добавим метод read_value_by_doc_id. Если интерфейс передать в метод get_value_plus_one, код будет выглядеть так:
CLASS lcl_example_class DEFINITION.
  PUBLIC SECTION.
    DATA test_mode TYPE abap_bool.
    DATA test_buffer TYPE TABLE OF ztable1.
    METHODS get_value_plus_one
      IMPORTING if_ztable1_reader   TYPE REF TO lif_ztable1_reader
iv_doc_id           TYPE char10
      RETURNING VALUE(return_value) TYPE i.
ENDCLASS.
CLASS lcl_example_class IMPLEMENTATION.
  METHOD get_value_plus_one.
    DATA lv_value TYPE i.
lv_value = if_ztable1_reader->read_value_by_doc_id( iv_doc_id ).
    IF lv_value IS NOT INITIAL.
return_value = lv_value + 1.
    ENDIF.
  ENDMETHOD.
ENDCLASS.
Что нам это дало?
Чтение из базы данных для метода do_something – это жёсткая зависимость. С помощью интерфейса мы разорвали зависимость, абстрагировав чтение данных.
Теперь мы можем сделать две реализации: обычную, читающую данные через select, и тестовый дубль, который будет возвращать тестовые данные из внутренней таблицы в режиме теста.
Тест будет выглядеть примерно так:
CLASS lcl_example_class_unit DEFINITION FOR TESTING RISK LEVEL HARMLESS.
  PUBLIC SECTION.
    METHODS run_test FOR TESTING.
ENDCLASS.
CLASS lcl_example_class_unit IMPLEMENTATION.
  METHOD run_test.
DATA(lo_cut) = NEW lcl_example_class( ).
DATA(lo_mock_reader) = NEW lcl_ztable1_mock_reader( ).
lo_mock_reader->add_rec_to_test_buffer( iv_doc_id = '0000000005'
iv_value  = 10 ).
DATA(lv_result) = lo_cut->get_value_plus_one( if_ztable1_reader = lo_mock_reader
iv_doc_id         = '0000000005' ).
cl_aunit_assert=>assert_equals( act = lv_result
                                    exp = 11 ).
ENDMETHOD.
ENDCLASS.
Способы дублирования объектов
Есть несколько подходов к тому, как реализовывать тестовые дубли объектов на ABAP.
Библиотека MockA
ABAP Test Double Framework – начиная с 7.4 sp9
ABAPTestSeam – начиная с версии 7.50
ABAPSQLTestDoubleFramework – начиная с 7.52 (к сожалению)
Также можно писать свой дубль вроде того, который представлен в примере выше.
По возможности я стараюсь создавать универсальные дубли, которые относятся не к одной разработке или не к одному вызову, а содержат универсальный код, применимый в различных тестах.
Например, можно написать класс, который позволяет читать и записывать в Z-таблицу lcl_database_reader, он может иметь методы:
Read_from_database
Write_to_database
Если в качестве одного из параметров сделать имя таблицы и сделать код достаточно универсальным, то можно использовать его в разработках для работы с БД в своих разработках вместо select, а затем написать его дубль и легко использовать данный класс в тестах.
Другой пример универсального дубля при написании кода для SAP BPC можно увидеть в моей статье Unit-тесты: универсальные
Если хотите прочитать статью полностью и оставить свои комментарии присоединяйтесь к sapland
ЗарегистрироватьсяУ вас уже есть учетная запись?
Войти
Обсуждения
2
	
Комментарий от
Александр Носов
| 19 апреля 2021, 14:11
Комментарий от
Александр Разинкин
| 21 апреля 2021, 06:10
Александр Носов 19 апреля 2021, 14:11
Смысл Unit-тестов заключается в том, чтобы автоматически тестировать код бизнес логики на простых сценариях. Вы же предлагаете изменить бизнес логику так, чтобы для тестов код проходил по одному сценарию, а для продуктива по другому.
METHOD get_value_plus_one.
DATA lv_value TYPE i.
lv_value = if_ztable1_reader->read_value_by_doc_id( iv_doc_id ).
IF lv_value IS NOT INITIAL.
return_value = lv_value + 1.
ENDIF.
ENDMETHOD.
Бизнес-логика одна, она в коде представлена один раз и проходит по одному сценарию:
Шаг 1. Считать значение по ID;
Шаг 2. Прибавить 1.