Эволюция Assert'a на примере тестирования вездехода из Звездных Войн |
11.02.2025 00:00 |
Меня зовут Михаил Палыга, я инженер в Блоке обеспечения и контроля качества выпуска изменений ПО в РСХБ‑Интех. Разработчик я начинающий, в компании работаю около года, плюс 6 лет обучения в профильном вузе. В данной статье хочу рассказать, как мы в нашем проекте проводим проверки данных в наших тестах. Мы занимаемся разработкой автотестов для АБС ЦФТ‑Банк — автоматизированной банковской системы (АБС), разработанной ООО «Центр финансовых технологий». Это ядро IT‑экосистемы банка. Тесты состоят из трех этапов: формирование условий, воздействие и проверка результата. Кстати, о том как и каким инструментом мы подбираем тестовые данные, мы писали ранее в этой статье, а об особенностях перехода с Oracle на PostgreSQL мы писали тут. На проекте для проверки данных мы пользуемся библиотекой AssertJ — Java библиотекой с открытым исходным кодом, используемой для написания гибких, содержательных и легко читаемых проверок в тестах Java. Мы любим использовать цепочки методов в других наших классах, поэтому данная библиотека органично вписалась в код наших тестов. Далее в статье я опишу, как со временем менялся наш подход к проведению проверок данных и как менялись сами классы проверок. А чтобы было чуть проще и интересней — займемся тестированием чего‑нибудь из вселенной Звездных Войн. Например, протестируем имперский бронированный транспортный вездеход AT‑AT. Схемы БД и код методов будет представлен в упрощенном виде. Представим, что в БД информация о нашем вездеходе хранится в одной таблице, без каких-либо связей: Давным-давно, в далеком-далеком проекте…«Это были тяжелые времена, мы проверяли данные как могли». А там, где и было возможно код переиспользовать, начали появляться длинные цепочки наследований: «Там, где‑то в одном Helper'е лежит нужная мне проверка, унаследуюсь‑ка я от него. А еще в другом классе тоже есть пара нужных мне методов, надо бы и его встроить в цепочку наследования». К тому же и сам подход к написанию методов проверки данных не стал стандартизирован. Творческая свобода – это, конечно, хорошо, но проверить данные с единым подходом и в едином стиле – еще лучше, не правда ли? Поэтому нам нужен был порядок… Новый порядок. Войны клоновНа новом этапе было решено перенести проверки из глубин наследований в отдельные классы, относящиеся к определенной сущности. Так на проекте появились классы Checker – они представляли из себя набор статичных методов с одной или несколькими проверками для передаваемого объекта. Например, по тесту нам надо проверить, что наш АТ-АТ соответствует следующим параметрам:
Так бы в теле теста выглядел наш метод проверки объекта:
Но допустим у нас появляется еще один тест на AT-AT, где надо проверить еще и место, где был произведен шагоход. Итого нам нужно проверить следующее:
Тут ярко видна проблема создания неатомарных проверок: либо сделаем копию старого метода и добавим туда еще один параметр, либо будем разбивать старый метод на отдельные проверки и добавлять еще и свою.
За время работы с классами типа Checker были выявлены следующие проблемы:
Описанные выше проблемы и желание выстроить наборы проверок в единый стиль с остальными нашими классами сподвигли нас на создание нового инструмента для проверок данных. Новая надеждаИзучив документацию по библиотеке AssertJ, мы предприняли попытку создать свои кастомные проверки. Оказалось, все довольно просто: наследуемся от абстрактного класса AbstractAssert и дело в шляпе (ну почти). Для большего удобства, чтобы всем на проекте было понятно, где искать нужный класс проверок для своего объекта, мы добавили единую точку входа для наших кастомных проверок – класс AssertionsUtil.
К примеру, метод проверки размера экипажа может выглядеть так:
А так будет выглядеть код проверки АТ-АТ в теле автотеста:
Пробуждение силы хардкодаВ процессе использования новых кастомных проверок встал вопрос о том, как проверять связанные классы. Нужен был способ как, проверяя объект А, перейти к проверкам связанного с ним объекта В – назовем это “погружением”. При этом должна была сохраниться возможность вернуться к проверкам объекта А – назовем это “подъемом наверх”. Давайте немного усложним нашу схему. Вынесем производителя и класс техники в отдельные таблицы: Первым делом в ход пошел старый добрый хардкод. Так выглядел метод погружения:
А так выглядел метод подъема наверх:
Однако, некоторый объект С мог быть связанным как с объектом А, так и с объектом В, следовательно, он должен иметь возможность подъема наверх к обоим классам. Например, производитель мог выпускать не только наземную технику, но и космические корабли. Соответственно, класс мог содержать сразу несколько методов “подъема наверх”. Так стала выглядеть проверка в теле автотеста:
При погружении разработчик будет вынужден держать в голове откуда он пришел, чтобы вызвать нужный метод подъема, либо мучаться каждый раз, когда программа будет падать с ошибкой CastException. Тем не менее, идея ходить по связанным объектам в рамках одной цепочки проверок нам понравилась, и мы решили развивать её дальше. Скрытая угроза классов-обертокДалее для реализации методов “подъема” мы опробовали Generic-методы. Уже стало лучше, не надо было держать в голове откуда ты по цепочке методов пришел в “нижнюю” проверку и как потом обратно возвращаться наверх. Теперь же появился универсальный метод and(), который возвращал нас точно туда, куда нам надо. Но у такого подхода был и минус. Каждый класс и все его методы приходилось оборачивать в класс-обертку. Выглядело это так:
Проверка нашего шагохода стала выглядеть так:
Писать обертки на каждый класс, и уж тем более на каждый метод было не очень весело, так что мы решили поискать еще варианты. Consumer’ы наносят ответный удар.На данный момент мы пришли к варианту с использованием Consumer’ов в параметрах метода погружения. Это позволило нам избавиться от классов-оберток и в целом от необходимости держать двустороннюю связь верхнего и нижнего класса. Теперь для “погружения” достаточно было создать один метод в верхнем классе:
Давайте еще разок доработаем нашу схему: вынесем экипаж в отдельную сущность и сделаем связь “один ко многим”. Большой проблемы добавить проверки списка не было. Для удобства мы сделали свой родительский класс BasicListAssert с набором своих базовых проверок на основе AbstractAssert. Помимо проверок списка нам надо было как-то осуществлять переход из этого списка к конкретному объекту. Для этого мы добавили метод с поиском по предикату: из списка отбирался первый подходящий объект и происходил переход к проверкам этого объекта. Также мы решили ввести классы с заготовками часто используемых наборов предикатов.
В итоге проверка нашего шагохода стала выглядеть так:
ЗаключениеТаким образом у нас получилось пройти путь от хаоса неатомарных проверок и методов-клонов к созданию мощного инструмента проверки данных, который позволяет проверять модели со связями 1-1, 1-Many, код был приведен к единому стилю, повышено удобство использования проверок и снижен к минимуму риск дублирования кода. Данный инструмент будет удобен для использования не только во вселенной звездных войн, но и в любой другой предметной области. |