Несколько лет назад, на проекте по разработке внутреннего портала для учебных заведений, занимающем практически весь датский рынок, я разработал прототип системы тестирования слоя данных, внедрил его в процесс разработки и обучил использованию две проектных команды. В данном посте я подведу итоги и расскажу об основных изменениях. Проведём небольшой экскурс в прошлое и повторимся. Данный фреймворк был разработан, опираясь на внутреннюю инфраструктуру кода и используемые технологии, с целью автоматизации и упрощения процесса тестирования сложных SQL-запросов. Он позволял протестировать отдельные SQL-запросы и установку свежей и чистой базы данных для нового клиента. Кроме того, он отлавливал неочевидные проблемы совместимости: сперва при обновлении сервера с MySQL 5.1 до MySQL 5.6, а затем при переходе на MariaDB. Изначально система использовалась на небольшом проекте на пару команд. Но её нужно было внедрять и в другие, более масштабные (до 15 команд!), проекты. Дело усложнялось тем, что это был единый модуль, развивающийся внутри первого проекта и ориентированный на фреймворк MSTest. Рекомендую ознакомиться с первой частью статьи, где подробно разобрана структура системы и механизмы её работы. В первое время после внедрения разработчики часто сообщали о проблемах и ситуациях, когда данный фреймворк было тяжело или невозможно использовать. Я всегда шёл навстречу и старался помогать им осваивать новый инструмент, а в свободное время исправлял и улучшал его. Следующим этапом развития системы стало внедрение на другой проект — более внушительный по размеру кода, структуре базы данных, сложности SQL-запросов и количеству разработчиков. Первое, что необходимо было решить, — это адаптация библиотеки под другой базовый фреймворк тестирования. На данном проекте использовался NUnit, в то время как библиотека была изначально разработана под MSTest. Требовалось разделить всю библиотеку на несколько сборок — DataLayerTests.Core, DataLayerTests.MSTestDriver, DataLayerTests.NUnitDriver — и собирать их раздельными NuGet-пакетами. В целом переход на другой фреймворк тестирования прошёл без сложностей, код был разделён и адаптирован. Основной проблемой создания адаптеров была идентификация текущего контекста тестирования для корректного определения атрибутов с метаданными, использующимися на классах и методах теста. В фреймворке MSTest для этого используется внедрение зависимости внутрь класса с помощью свойства TestContext, в то время как для фреймворка NUnit данная информация доступна только с помощью глобальной переменной TestContext.CurrentContext. С помощью паттерна Адаптер мы объединили их под общий интерфейс и внедрили в зависимости базового класса тестирования. Благодаря такому разделению, получилось минимизировать дублирование кода, и в итоге пакеты адаптеров получились небольшими, а основная логика фреймворка для тестирования осталась в базовом пакете. Самое главное — это поддержание корректной версии пакетов: т.к. мы внесли несовместимые изменения, то пришлось поднять номер до 2.0. Первоначальный набор правил генерации тестовых сущностей включал в себя правило ForeignKeyRule, которое позволяет создавать связанную сущность. Данное правило покрывало потребности разработчиков при написании новых тестов для разных запросов и структур данных. Но в один прекрасный момент мы всё же столкнулись с ситуаций, когда его было не достаточно. У нас была следующая схема таблиц, которые необходимо было заполнить для тестирования очередного запроса: Наша задача заключалась в формировании разных настроек для GroupEntity и PersonGroupEntity одновременно, при этом отношение GroupEntity к CourseEntity было «один-к-одному», в то время как PersonGroupEntity к GroupEntity — «много-к-одному». Мы перепробовали множество различных способов для заполнения необходимых данных с помощью тех инструментов, которые предлагала библиотека на данном этапе развития. Очевидное решение, которое бы всех устраивало, не получилось найти быстро, поэтому тестовые данные были заполнены вручную. Спустя время появилась идея создания нового правила — аналогичного ForeignKeyRule, но с точностью до наоборот. Я назвал его InsideOutForeignKeyRule.  Если для ForeignKeyRule сперва производится вставка дополнительной сущности в базу данных, а затем осуществляется её привязка через присвоение новых полученных идентификаторов, то в новом правиле всё было наоборот: сперва происходила вставка основной сущности, а затем — связывание и вставка новой сущности. Таким образом, благодаря новому правилу стало возможно писать лаконичный код генерации сущностей. Изначально, когда разрабатывался данный фреймворк, было допущение, что в основном работа происходит с одним типом данных, поэтому хранилище сгенерированных объектов представляло собой простой типизированный список, который на последнем этапе отправлялся в базу данных. Все правила заполнения и генерации сущностей имели простой интерфейс только с одним методом Invoke(EntityRuleContext context), который мог изменить сущность до вставки в базу. Но в то же время были добавлены и более сложные правила, которые генерировали новые сущности другого типа — ForeignKeyRule. Это правило генерировало новую сущность, вставляло её в базу и использовало полученный новый идентификатор для записи в поле родительской сущности. Несложно догадаться, что код, представленный ниже, генерирует 21 запрос на вставку данных, что может значительно снижало производительность тестов. (1*) 20 сущностей BookEntity создаются и складываются в память, последовательно используя объявленные ранее правила. Соответственно, последовательно вызывается правило UseForeignKeyRule, которое не использует промежуточное хранилище, а сразу же отправляет новую сущность в базу данных. В результате получаем 20 запросов на вставку одиночных сущностей, и после вызова InsertAll() получаем последний 21-ый запрос для вставки 20 сгенерированных сущностей BookEntity. Когда я изначально проектировал и разрабатывал систему тестирования, я понимал данную проблему, но сперва хотелось опробовать разработанное решение на реальном проекте, получить обратную связь и продолжить развитие. Теперь, когда я вижу, что библиотека выполняет свои функции хорошо и не вызывает критичных проблем, я могу позволить себе заниматься оптимизацией и совершенствованием.  В новой версии было разработано универсальное хранилище, которое позволяет получить доступ к любому типу сущностей. Но это не главная его особенность. Основная — это формирование и группировка запросов вставки данных. Раньше фреймворк генератора сущностей манипулировал доступом к базе данных с помощью непосредственного доступа. Это осложняло процесс переработки генератора в контексте разработки механизма отложенной записи. Поэтому все необходимые методы были вынесены в интерфейс, который был реализован специальным временным хранилищем и классом, работающим напрямую с базой данных. Для того чтобы реализовать механизм отложенной вставки связанных сущностей, необходимо было пересмотреть механизм описания новых правил формирования данных. Старый метод Invoke был помечен как устаревший, и теперь присутствуют два новых — OnBeforeInsert и OnAfterInsert, которые вызываются соответственно до и после вставки сущности в базу данных. А также появилась возможность подписаться на событие вставки сущности в базу данных, чтобы получить обновлённое значение Auto Increment поля или вставить сущность с явным ограничением в базе по вторичному ключу. В большинстве случаев правила генерации остались без изменений логики, а вот правило генерации связанной сущности необходимо было переработать под новую архитектуру. Давайте посмотрим, как оно работало раньше и как оно стало работать сейчас. Если на тот момент времени разработанный синтаксис построителя сущностей меня полностью устраивал, то спустя несколько лет написания тестов он казался очень громоздким и сложным для восприятия. Новые разработчики, с которыми мне доводилось общаться, подтвердили мои ощущения. Даже я, возвращаясь спустя несколько месяцев к уже написанным тестам, тратил немало времени, чтобы понять, что именно в них происходит.  После переосмысления внутренней логики формирование сущностей сделало построители простыми и гибкими. Был потрачен не один день на придумывание и согласование нового формата, и в итоге удалось найти удобный и компактный синтаксис, который улучшает восприятие при ознакомлении с кодом тестов спустя время. Ознакомимся с проблемами старого формата подробнее, чтобы вы могли понять, почему сделаны те или иные изменения, и проследить их развитие. Длинные именования методов, которые могут путать разработчика. Сложность управления видимостью правил создания сущностей. Невозможность работы с множеством сущностей за один раз. Рассмотрим следующий код, чтобы понять суть проблем. Как видите, проблема заключается в том, что все правила линейные, никак не группируются, объявления для основных сущностей выглядят просто, а для всех остальных появляется много лишнего. При добавлении областей видимости правил, которые позволяют переопределять данные сущностей временно, код становился ещё сложнее для восприятия и отладки. Рассмотрим на примере:   Из-за линейности подхода становится сложно выделить, что к чему относится. Дополнительные отступы помогают, но только до тех пор, пока не произойдёт автоформатирование кода. В итоге, мы пришли к следующему: если раньше нужно было каждый раз явно создавать новый экземпляр построителя для каждой сущности и дальше работать с линейной цепочкой методов, то сейчас мы отказались от такого подхода и перешли в сторону набора функций конфигураторов.  Если раньше всегда приходилось писать конструкцию следующего вида: То сейчас всё сводится к вызову одного метода: Я хотел упростить жизнь разработчикам и реализовать подсказки, группировку правил и логики с помощью синтаксиса языка. Поэтому я разделил логику на набор разных интерфейсов, из них два основных: IScopedEntityBuilder — предоставляет доступ к методам создания сущностей, методам формирования групп и методам настройки поведения правил по умолчанию. INestedEntityBuilder — даёт возможность определить правила для сущностей.  Каждый из этих интерфейсов абстрактен и позволяет создавать правила для любых типов сущностей, а также даёт возможность упростить код, предоставив вариацию для конкретной сущности. Пример 1. Простое создание сущностей: правила задаются только для одной сущности, добавить правила для других не получится, так же как и создать явно другие: Пример 2. Конфигурируем несколько сущностей: Данный пример выглядит приятно, но при большем количестве правил для разных сущностей может выглядеть странно — данные могут перемешиваться, поэтому необходимо ограничить зоны определения данных. Для этого можно воспользоваться следующим форматом:       Как видите, теперь правила сгруппированы, и компилятор не позволит разработчику смешивать их между собой. Это упрощает чтение тестов, а при расширении становится очевидным, куда добавлять новые правила. Пример 3. Области видимости правил. Таким образом, код генерации данных становится понятнее, и самое главное, что компилятор и IDE могут подсказывать разработчику, куда и где необходимо добавлять правила. Как итог, библиотека развивается и помогает отлавливать самые необычные и странные ошибки при написании сложных SQL-запросов, покрывая их множественными комбинациями данных. Она также помогла проверить и отладить неожиданные проблемы при миграции со старых версий MySQL на MariaDB. Сейчас на одном из проектов уже насчитывается порядка 1000 тестов, и все они выполняются в течение первых 4 минут. Учитывая сложность структуры базы данных, я считаю это неплохим результатом. Веб разработчик