Методология TDD (Test-Driven Development), или разработка через тестирование, давно стала стандартом в так называемом «тру-программировании» (Java, C#, C++). Однако в мире 1С вокруг неё до сих пор не затихают жаркие споры. Многие разработчики считают TDD в 1С утопией, дорогой игрушкой или концептом, неприменимым на практике из-за особенностей платформы. Другие же успешно внедряют модульные тесты и утверждают, что это в разы повышает качество и скорость разработки.
В этой статье мы подробно разберем, почему классический подход TDD сложно приживается в экосистеме «1С:Предприятие», в чем разница между BDD и модульными тестами, как правильно проектировать код для тестирования и какие современные инструменты позволяют автоматизировать этот процесс.
Прежде чем перейти к специфике 1С, вспомним суть методологии TDD, сформулированную Робертом Мартином (Дядюшкой Бобом). Она базируется на трех строгих законах:
Этот цикл (красный – зеленый – рефакторинг) в идеале длится от 30 секунд до нескольких минут. Сначала мы пишем тест на несуществующую функцию, запускаем его — он падает (красный). Затем пишем минимальный «грязный» код, лишь бы тест прошел (зеленый). После этого проводим рефакторинг кода, приводя его к красивому виду, и убеждаемся, что тесты остаются зелеными. Такой подход кардинально меняет мышление: разработчик проектирует интерфейс взаимодействия (API) еще до того, как приступает к реализации алгоритма.
Рассмотрим главные технологические препятствия, которые мешают классическому модульному тестированию в 1С:
Часто при упоминании тестирования в 1С разработчики сразу вспоминают экосистему Vanessa-Behavior (BDD). Однако важно понимать фундаментальную разницу между ними.
BDD (Behavior-Driven Development) — это сценарное тестирование поведения системы с точки зрения пользователя. Тесты пишутся на человекочитаемом языке Gherkin (сценарии «Дано - Когда - Тогда»). Такие тесты запускают интерактивный клиент, кликают по кнопкам на формах и проверяют конечный результат. Это отличный инструмент контроля качества релиза, но он обладает огромными минусами для разработчика:
TDD (Unit-тестирование) оперирует чистыми функциями. Тесты выполняются за миллисекунды. Они проверяют изолированные участки бизнес-логики без открытия форм и интерфейсных задержек. Только комбинация быстрых юнит-тестов (для разработчика в процессе кодинга) и прикладных BDD-тестов (для проверки сквозных бизнес-сценариев) дает максимальный эффект.
Чтобы код в 1С можно было покрыть модульными тестами, его необходимо правильно спроектировать. Рассмотрим важнейшие правила декомпозиции, которые помогают избавиться от «запахов кода»:
Правило 1: Разделяйте получение данных и вычисления.
Если ваша функция делает запрос к базе данных, а затем на основе результата выполняет сложный расчет — разбейте её на две функции. Первая функция делает запрос и возвращает простую структуру или таблицу значений. Вторая (чистая) функция принимает эту таблицу на вход, производит математические вычеты и возвращает результат. Вторую функцию вы сможете легко протестировать в изоляции, подавая на вход любые искусственные таблицы значений без обращения к реальной БД.
Правило 2: Минимизируйте сигнатуру методов.
Процедуры с десятком входящих параметров крайне сложны для тестирования. Стремитесь к тому, чтобы функция выполняла ровно одно действие (принцип единственной ответственности) и принимала на вход минимальное количество простых параметров.
Правило 3: Избегайте «стрельбы дробью» (Shotgun Surgery).
Если при изменении одного бизнес-коэффициента вам приходится переписывать формулы в десяти разных модулях документов — это архитектурная ошибка. Инкапсулируйте расчеты в одном общем модуле и тестируйте только этот модуль.
Рассмотрим классический пример разработки через тестирование. Представим, что нам нужно написать функцию, которая вычисляет следующий рабочий день с учетом выходных и праздников. На входе — дата, на выходе — следующая рабочая дата.
Следуя первому закону TDD, мы сначала создаем тестовый случай (Assert). Для этого мы используем современный фреймворк тестирования (например, YAxUnit). Наш тест должен выглядеть так:
Процедура Тест_СледующийРабочийДень_ДолженВозвращатьПонедельникДляСубботы() Экспорт
// Arrange (Подготовка данных)
ТестоваяДата = Дата(2020, 10, 3); // Суббота
ОжидаемаяДата = Дата(2020, 10, 5); // Понедельник
// Act (Действие)
ФактическаяДата = КалендарныеВычисления.СледующийРабочийДень(ТестоваяДата);
// Assert (Проверка результатов)
ЮТТесты.ОжидаетЧто(ФактическаяДата).Равно(ОжидаемаяДата);
КонецПроцедуры
На данном этапе общего модуля КалендарныеВычисления еще не существует, либо в нем нет метода СледующийРабочийДень. Попытка запустить тест вызовет ошибку компиляции. Это и есть «отказной тест» (красный шаг).
Теперь мы пишем минимально необходимый код в общем модуле КалендарныеВычисления, чтобы тест скомпилировался и прошел:
Функция СледующийРабочийДень(ИсходнаяДата) Экспорт
// Минимальный код для прохождения первого теста
Возврат ИсходнаяДата + 86400 * 2;
КонецФункции
Наш тест стал зеленым. Но мы понимаем, что код не универсален. Пишем второй тест для обычной среды (например, для вторника):
Процедура Тест_СледующийРабочийДень_ДолженВозвращатьСредуДляВторника() Экспорт
ТестоваяДата = Дата(2020, 10, 6); // Вторник
ОжидаемаяДата = Дата(2020, 10, 7); // Среда
ФактическаяДата = КалендарныеВычисления.СледующийРабочийДень(ТестоваяДата);
ЮТТесты.ОжидаетЧто(ФактическаяДата).Равно(ОжидаемаяДата);
КонецПроцедуры
Запускаем — второй тест падает. Теперь мы переписываем функцию СледующийРабочийДень так, чтобы проходили оба теста:
Функция СледующийРабочийДень(ИсходнаяДата) Экспорт
ДеньНедели = ДеньНедели(ИсходнаяДата);
Если ДеньНедели = 5 Тогда // Пятница
Возврат ИсходнаяДата + 86400 * 3;
ИначеЕсли ДеньНедели = 6 Тогда // Суббота
Возврат ИсходнаяДата + 86400 * 2;
Иначе
Возврат ИсходнаяДата + 86400;
КонецЕсли;
КонецФункции
Оба теста проходят. Теперь мы можем безопасно провести рефакторинг этого кода (например, задействовать производственный календарь из базы данных или константы), зная, что если мы случайно сломаем базовую логику расчета дней недели, наши тесты мгновенно об этом сообщат.
Рассмотрим более сложный случай — функцию расчета возраста физического лица на определенную дату. Типичный разработчик 1С напишет функцию, которая принимает ссылку на справочник ФизическиеЛица, делает внутри запрос к базе данных, получает дату рождения и вычисляет разницу. Такую функцию крайне неудобно тестировать: нам придется создавать тестовый элемент справочника в реальной базе данных перед каждым прогоном теста.
Применим правильную декомпозицию. Разделим задачу на две части:
Напишем чистую функцию расчета возраста в общем модуле ФизическиеЛицаКлиентСервер:
Функция ВозрастНаДату(ДатаРождения, ДатаРасчета) Экспорт
Если Не ЗначениеЗаполнено(ДатаРождения) Или ДатаРождения > ДатаРасчета Тогда
Возврат 0;
КонецЕсли;
Возраст = Год(ДатаРасчета) - Год(ДатаРождения);
// Проверяем, был ли уже день рождения в текущем году
Если ДобавитьМесяц(ДатаРождения, Возраст * 12) > ДатаРасчета Тогда
Возраст = Возраст - 1;
КонецЕсли;
Возврат Возраст;
КонецФункции
Теперь мы можем написать целую батарею быстрых модульных тестов, проверяющих все возможные граничные условия (обычные дни, високосные годы, дни рождения 29 февраля, не заполненные даты), вообще не обращаясь к базе данных 1С:
Процедура Тест_ВозрастНаДату_ГраничныеСлучаи() Экспорт
// Проверка стандартного расчета
ЮТТесты.ОжидаетЧто(ФизическиеЛицаКлиентСервер.ВозрастНаДату(Дата(1980, 2, 12), Дата(2022, 2, 12))).Равно(42);
// Проверка за день до дня рождения
ЮТТесты.ОжидаетЧто(ФизическиеЛицаКлиентСервер.ВозрастНаДату(Дата(1980, 2, 12), Дата(2022, 2, 11))).Равно(41);
// Проверка для родившихся в високосный год (29 февраля) в невисокосный год
ЮТТесты.ОжидаетЧто(ФизическиеЛицаКлиентСервер.ВозрастНаДату(Дата(1980, 2, 29), Дата(2021, 2, 28))).Равно(40);
ЮТТесты.ОжидаетЧто(ФизическиеЛицаКлиентСервер.ВозрастНаДату(Дата(1980, 2, 29), Дата(2021, 3, 1))).Равно(41);
// Проверка пустой даты рождения
ЮТТесты.ОжидаетЧто(ФизическиеЛицаКлиентСервер.ВозрастНаДату(Дата(1, 1, 1), Дата(2022, 2, 12))).Равно(0);
КонецПроцедуры
Эти тесты выполняются за доли миллисекунды. Они гарантируют, что логика расчета возраста работает абсолютно верно во всех мыслимых астрономических сценариях. Основная функция, принимающая ссылку, будет выглядеть предельно просто и тривиально, её тестирование уже не потребует сложного покрытия кейсами:
Функция ВозрастФизическогоЛица(ФизическоеЛицо, ДатаРасчета) Экспорт
ДатаРождения = ОбщегоНазначения.ЗначениеРеквизитаОбъекта(ФизическоеЛицо, "ДатаРождения");
Возврат ВозрастНаДату(ДатаРождения, ДатаРасчета);
КонецФункции
Если раньше разработчики 1С страдали от «EPF-ада» (когда тесты приходилось хранить во внешних файлах обработок, которые постоянно терялись и не версионировались в Git), то сегодня экосистема тестирования сделала огромный шаг вперед.
Проанализируем ключевые современные технологии автоматизации тестирования в 1С:
.cfe) для хранения тестов.Внедрение TDD — это не вопрос наличия идеальных инструментов в платформе 1С, это вопрос **культуры разработки** и отношения к качеству кода. Практика показывает, что затраты на написание тестов полностью окупаются на долгой дистанции за счет снижения стоимости исправления ошибок в продакшене.
Начните с малого: перестаньте писать гигантские нелинейные процедуры. Начните выделять чистые вычислительные функции в общие модули и покрывать их простыми проверками утверждений. Вы увидите, как ваш код станет чище, архитектура — гибче, а боязнь рефакторинга перед выпуском релиза исчезнет навсегда.