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.