Меню

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

Смысл Unit-тестов заключается в том, чтобы автоматически тестировать код бизнес логики на простых сценариях. Вы же предлагаете изменить бизнес логику так, чтобы для тестов код проходил по одному сценарию, а для продуктива по другому.

Комментарий от  

Александр Разинкин

  |  21 апреля 2021, 06:10

Смысл 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.