| name | yaxunit-test-writer |
| description | Написание юнит-тестов для конфигураций 1С на фреймворке YAxUnit. Используй этот скилл всегда, когда нужно написать, добавить или исправить тесты для модулей 1С — общих модулей, документов, справочников, регистров и т.д. Триггеры: "напиши тест", "добавь тест", "покрой тестами", "юнит-тест для", "тест для модуля", "яксюнит", "yaxunit", а также когда пользователь просит проверить логику метода через тест.
|
| argument-hint | [srs-NNN] |
YAxUnit Test Writer
- Пишешь тесты на BSL для фреймворка YAxUnit в конфигурации 1С:Предприятие.
- Тесты — расширение конфигурации с именем Тесты.
- Проектный тег (если передан явно при вызове): $ARGUMENTS (если не передан — определи из контекста разговора).
- Версия платформы 1С: прочитай из CLAUDE.md проекта (строка «Версия платформы для тестирования»). Используй эту версию для выбора XML-шаблонов из папки
templates/.
- Предполагается, что тесты запускаются на пустой информационной базе данных.
Читай нужный файл по мере работы над тестом:
| Когда нужен | Что делать |
|---|
| Нужно определить имя нового тестового модуля | Читай references/module-naming.md |
| Расставляешь теги или определяешь уровень тегирования | Читай references/tags.md |
Тестовый модуль клиент-серверный (ClientManagedApplication=true) | Читай references/compilation-contexts.md |
| Пишешь проверки (// then) | Читай references/framework/assertions.md |
| Создаешь тестовые данные, используешь хуки или контекст | Читай references/framework/test-data.md |
| Изолируешь зависимости от БД или внешних сервисов | Читай references/framework/mocking.md |
Мокируешь метод и убедился, что &Вместо для него в расширении отсутствует | Читай references/interceptors.md |
Тестируемый метод вызывает ПолучитьФункциональнуюОпцию() | Раздел «Тестирование методов с ПолучитьФункциональнуюОпцию()» в основном тексте скилла |
| Проверяешь сигнатуру платформенного метода или типа | Используй MCP 1c-platform: search, info, getMember, getMembers, getConstructors |
Процесс написания теста
1. Подготовка модуля
Подготовь тестовый модуль по разделу «Структура файлов теста». Если модуль уже существует — прочитай существующий Module.bsl, чтобы знать какие тесты уже написаны.
2. Анализ и проектирование
- Прочитай тестируемый метод целиком — обязательно перед проектированием тестов
- Определи тип каждого теста по разделу «Типы тестов»
- Сформируй имена тестовых методов по разделу «Именование тестов»
- Если имя получается неочевидным — добавь
// TODO: пояснение в заготовку
3. Фиксация заготовок
Запиши в файл до начала реализации:
- Регистрацию в
ИсполняемыеСценарии (см. «Способы объявления тестов»)
- Пустые экспортные процедуры с комментариями
// given, // when, // then (см. «Структура тестового модуля»)
4. Реализация
Пиши тела тестовых методов. Какие references читать — по таблице в начале скилла.
5. Организация
Приведи структуру регистрации и модуля в порядок с учетом всех тестов (старых + новых):
- Все тесты интеграционные →
.ВТранзакции() на уровне модуля или единственного набора
- Есть и интеграционные, и юниты → разбей на наборы; интеграционному набору — тег
"Интеграционный" и .ВТранзакции()
- Тестов более 5 → группируй по логическим группам в наборы для читаемости
- Реализации тестов из набора оборачивай в
#Область с именем набора
- Проставь теги (см.
references/tags.md)
- Проверь по чеклисту в конце скилла
Структура файлов теста
- Тесты должны располагаться в директории
tests.
- Тесты - это общие модули и должны быть расположены в
tests/CommonModules
- Каждый тестовый модуль должен быть назван по шаблону
[Префикс типа объекта_][Имя проверяемого объект][_Суффикс типа модуля].
Как назвать модуль — references/module-naming.md.
test/
CommonModules/
[ИмяТестовогоМодуля].xml ← описание модуля (метаданные)
[ИмяТестовогоМодуля]/
Ext/
Module.bsl ← код теста
Configuration.xml ← регистрация всех модулей
Перед созданием — проверь, существует ли уже модуль с таким именем (test/CommonModules/[ИмяТестовогоМодуля].xml). Если файл есть, модуль уже зарегистрирован: не трогай .xml и Configuration.xml, работай только с Module.bsl.
При создании нового модуля:
-
Создай test/CommonModules/[ИмяТестовогоМодуля]/Ext/Module.bsl
-
Добавь запись в test/Configuration.xml: в секции <ChildObjects> среди тегов <CommonModule> в алфавитном порядке:
<ChildObjects>
...
<CommonModule>ОМ_Колеровка</CommonModule>
<CommonModule>[ИмяТестовогоМодуля]</CommonModule>
<CommonModule>ОМ_МерныеТоварыВызовСервера</CommonModule>
...
</ChildObjects>
-
Создай файл test/CommonModules/[ИмяТестовогоМодуля].xml. {uuidgen} — уникальный UUID объекта метаданных; каждый модуль должен иметь свой UUID. Для генерации на Windows используй PowerShell:
powershell -Command "[guid]::NewGuid().ToString()"
Значения ClientManagedApplication и ClientOrdinaryApplication зависят от типа тестируемого объекта:
- ОМ_ (общий модуль) — прочитай
src/CommonModules/[ИмяТестируемогоМодуля].xml и скопируй значения ClientManagedApplication и ClientOrdinaryApplication оттуда. Тест должен быть доступен в тех же контекстах, что и тестируемый код.
- Остальные типы (Док_, РС_, Спр_ и др.) — всегда
false/false (только сервер).
Создай файл инструментом Write напрямую — никаких дополнительных скриптов не нужно. Отступы — табуляция.
Прочитай шаблон из templates/{версия-платформы}/common-module.xml и подставь значения: {uuidgen} → новый UUID, [ИмяТестовогоМодуля] → имя модуля, ??? → значения из правил выше.
Типы тестов
Перед написанием определи тип каждого теста — это влияет на изоляцию зависимостей, необходимость .ВТранзакции() и итоговую структуру регистрации.
Бизнес-слой — общие модули (префикс ОМ_)
Содержат бизнес-правила и оркестрацию. При грамотном проектировании не обращаются к БД напрямую — делегируют это менеджерам объектов. Тесты для ОМ_ — классические юнит-тесты:
- Слой данных (регистры, справочники) мокируется через Мокито
- Библиотечные методы и методы других компонентов мокируются через Мокито
- Без обращений к БД → без
.ВТранзакции()
- Быстрые, детерминированные
// ОМ_ тест: мокируем регистр, тестируем бизнес-логику
Мокито.Обучение(РегистрыСведений.ЦеныНоменклатуры)
.Когда("ПолучитьЦеныНоменклатур")
.Вернуть(МоковыеДанные)
.Прогон();
Результат = Ценообразование.РассчитатьЦену(Параметры);
Слой объектов метаданных — модули менеджеров и объектов (_ММ, _МО, Док_, Спр_, РС_)
Содержат логику работы с данными: запросы, запись, обработка событий объекта (ПередЗаписью, ОбработкаПроверкиЗаполнения). Тип теста определяется тем, обращается ли конкретный метод к БД:
- Интеграционный (метод обращается к БД: запросы, запись, чтение регистров) — работает с реальными данными, требует
.ВТранзакции() для отката
- Компонентный (метод обращается к библиотечным методам, методам других бизнес-правил) — если замокирован, то без транзакции; если мокирование невозможно и вызываемый метод обращается к БД — в транзакции
- Юнит (метод не обращается к БД) — например,
ОбработкаПроверкиЗаполнения, которая проверяет только состояние объекта в памяти — без транзакции
Структура тестового модуля
#Область СлужебныйПрограммныйИнтерфейс
Процедура ИсполняемыеСценарии() Экспорт
ЮТТесты
.Тег("srs-NNN") // необязательно
.ДобавитьТест("ИмяТеста1")
.ДобавитьТестовыйНабор("ИмяНабора", "ТипТеста") // необязательно — только если группируешь тесты в набор; тег типа теста — если применимо
.ДобавитьТест("ИмяТеста2")
.ДобавитьТест("ИмяТеста3")
.СПараметрами(Вход1, Ожидание1)
.СПараметрами(Вход2, Ожидание2);
КонецПроцедуры
Процедура ИмяТеста1() Экспорт
// given
// when
// then
КонецПроцедуры
#Область ИмяНабора // если есть набор — реализации его тестов оборачиваются в область с именем набора
Процедура ИмяТеста2() Экспорт
// ...
КонецПроцедуры
Процедура ИмяТеста3(Вход, Ожидание) Экспорт
// ...
КонецПроцедуры
#КонецОбласти
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
Функция СоздатьТовар(Наименование, Родитель = Неопределено)
// вспомогательный код для подготовки тестовых данных
КонецФункции
#КонецОбласти
Правила
ИсполняемыеСценарии — только регистрация тестов, никаких данных внутри
- Все тестовые процедуры — экспортные
- Вспомогательные методы (helper-методы) размещаются в области
СлужебныеПроцедурыИФункции — они не экспортные. Типичные helper-методы: создание тестовых объектов (СоздатьГруппу, СоздатьТовар), построение коллекций (КолеровочнаяКарта, Товары), добавление строк (ДобавитьВКарту)
Регистрация тестов
Именование тестов
Имя теста строится по схеме [Метод][Результат][Условие] слитно, в CamelCase:
МетодРезультатУсловие
- Метод — имя тестируемого метода или действия
- Результат — что делает или возвращает (
Возвращает, НеВыполняет, Выбрасывает, Сортирует)
- Условие — при каких обстоятельствах (
ПриОтсутствииДанных, ЕслиЭтоУслуга, ПриДублировании)
Примеры:
ПолучитьЦенуВозвращаетПустоеЗначениеПриОтсутствииДанных
ЗаписатьНеВыполняетЗаписьПриОшибкеПроверкиУникальности
ЭтоКолеровкаВозвращаетИстинуЕслиЭтоУслугаКолеровки
ПередЗаписьюСортируетДиапазоныПоВерхнейГранице
Способы объявления тестов
ЮТТесты
.Тег("srs-123")
.ДобавитьТест("ИмяМетода") // тест на всех контекстах
.ДобавитьСерверныйТест("ИмяМетода") // только сервер
.ДобавитьКлиентскийТест("ИмяМетода") // только клиент
.ДобавитьТестовыйНабор("ТестовыйНабор", "Теги") // группа тестов по критерию
.ДобавитьТест("ИмяМетода", "Читаемое описание") // с читаемым именем
.ДобавитьТест("ИмяМетода", , "Интеграционный") // интеграционный (тег, 3-й параметр)
.ДобавитьТест("ИмяМетода") // параметризованный тест
.СПараметрами(Арг1, Арг2)
.СПараметрами(Арг3, Арг4);
Именование тестовых наборов
YAxUnit поднимает каждый тестовый набор в корень дерева прогона как самостоятельный узел (независимо от модуля). Из-за этого верхний уровень дерева превращается в кашу: у модулей без наборов в корне видно имя модуля (Док_X_МО), а у модулей с наборами — голое название набора («Ветвление»), без привязки к объекту.
Правило: имя набора = <ИмяМодуля>: <аспект поведения>. Наборы обязательны — оборачивай тесты хотя бы в один набор.
.ДобавитьТестовыйНабор("Док_ПеремещениеТоваров_МО: Условия запуска формирования планов")
.ДобавитьТестовыйНабор("Док_ПеремещениеТоваров_МО: Формирование планов")
Почему так:
- префикс = имя модуля → оба случая (модуль без наборов и наборы) в корне начинаются одинаково, дерево единообразное, по имени модуля всё кластеризуется и ищется;
- префикс выводится механически из имени модуля — не нужно придумывать «человеческое» название;
- из узла дерева сразу понятно, в каком файле тест; имена наборов не сольются между модулями.
Аспект после : пиши как обычную фразу (регистр первого символа не меняй ради префикса). Имена #Область для группировки кода внутри файла — по аспекту, без префикса.
Правила формулировки аспекта:
Аспект — короткая русская фраза о поведении, не имя тестируемого метода, не имя функциональной опции, не технический ярлык:
// ❌ Плохо — имена методов, ФО и ярлыки: узлы неинформативны и совпадают между
// модулями (реальная коллизия: два неотличимых узла «ФункциональныеОпции»)
.ДобавитьТестовыйНабор("ФункциональныеОпции")
.ДобавитьТестовыйНабор("CRUD")
.ДобавитьТестовыйНабор("ИспользоватьПодразделенияПунктаВыдачиЗаказа")
// ✅ Хорошо — фраза о поведении
.ДобавитьТестовыйНабор("РС_ПоставщикиНекондиции_ММ: операции с записями")
.ДобавитьТестовыйНабор("ОМ_УправлениеЗаказами: влияние функциональной опции ИспользоватьПодразделенияПунктаВыдачиЗаказа")
Никаких наборов-заголовков без тестов. Вложенных наборов в YAxUnit не существует: «вложенный» набор — просто следующий элемент плоского списка, а набор без тестов выбрасывается при фильтрации, и его «дети» остаются в корне дерева без контекста. Псевдо-уровень выражай внутри аспекта через тире:
// ❌ Плохо — «Согласование» без своих тестов исчезнет из дерева,
// остальные наборы повиснут в корне сами по себе
.ДобавитьТестовыйНабор("Согласование")
.ДобавитьТестовыйНабор("Процесс согласования")
.ДобавитьТест("...")
.ДобавитьТестовыйНабор("Поиск записей")
.ДобавитьТест("...")
// ✅ Хорошо — уровень влит в аспект
.ДобавитьТестовыйНабор("РС_ДокументыКСогласованию_ММ: Согласование — процесс")
.ДобавитьТест("...")
.ДобавитьТестовыйНабор("РС_ДокументыКСогласованию_ММ: Согласование — поиск записей")
.ДобавитьТест("...")
Теги и режимы не кодировать в имени набора — для них есть колонки «Теги» и «Контекст» в форме запуска и метки в Allure-отчете:
// ❌ Плохо — тег продублирован в имени
.ДобавитьТестовыйНабор("Процесс согласования (интеграционные)")
// ✅ Хорошо — тег передан отдельным параметром
.ДобавитьТестовыйНабор("РС_ДокументыКСогласованию_ММ: Согласование — процесс", "Интеграционный")
Транзакция
.ВТранзакции() откатывает все изменения в БД после теста. Используй для тестов, которые записывают данные.
Транзакция на весь модуль
Когда все тесты в модуле интеграционные (пишут в БД, файловые операции и т. д.):
ЮТТесты
.ВТранзакции() // все тесты ниже выполняются в транзакции
.ДобавитьТест("ЗаписатьВыполняетЗаписьБезОшибок")
.ДобавитьТест("ЗаписатьНеВыполняетЗаписьПриОшибке")
;
Транзакция на отдельный тест
Когда часть тестов работает с БД, часть нет:
ЮТТесты
.ДобавитьТест("ПоискВозвращаетПустойРезультат") // без транзакции: не пишет в БД
.ДобавитьТест("ПоискНаходитСозданныйЭлемент")
.ВТранзакции() // пишет в БД → нужна транзакция
.ДобавитьТест("ПроверкаЗаполнения") // без транзакции: объект не сохраняется
;
Транзакция в группе тестов
.ВТранзакции() после .ДобавитьТестовыйНабор() применяется ко всем тестам набора:
ЮТТесты
.Тег("srs-123")
.ДобавитьТестовыйНабор("Запись данных")
.ВТранзакции()
.ДобавитьТест("Тест1")
.ДобавитьТест("Тест2")
.ДобавитьТестовыйНабор("Чтение данных") // без транзакции
.ДобавитьТест("Тест3")
;
Когда ставить .ВТранзакции()
| Ситуация | Транзакция |
|---|
Тест создаёт/изменяет записи через КонструкторОбъекта(...).Записать() | да |
| Тест вызывает метод, который пишет в БД, файловые операции и т.д. | да |
| Тест только читает данные или работает с несохранённым объектом | нет |
| Тест с мокированием (без реальной БД, файловых операций и т. д.) | нет |
Тест устанавливает константу и проверяет результат через ПолучитьФункциональнуюОпцию() | нет — значение ФО читается из внетранзакционного кеша, запись константы внутри транзакции невидима для ФО |
Маркировка интеграционных тестов
Тест, работающий с реальной БД, помечается тегом "Интеграционный". Уровень простановки тега — модуль, набор или отдельный тест — определяй по правилам в references/tags.md (раздел «Тег Интеграционный: на каком уровне ставить»).
Практики написания тестов
Данные в тесте — без зависимости от тестируемого кода
Никогда не используй методы тестируемого модуля для подготовки входных данных теста. Это хрупко: если реализация вспомогательного метода изменится, тест сломается независимо от того, правильно ли работает тестируемый метод.
Все структуры, контракты и коллекции, которые тест передаёт как входные данные, строй напрямую — через Новый Структура, Новый Массив, Новый Соответствие и конструкторы объектов:
// ❌ Плохо — зависимость от реализации ВнешняяСистемаИнтеграция.Ошибки_Инициализация
Ошибки = ВнешняяСистемаИнтеграция.Ошибки_Инициализация();
ВнешняяСистемаИнтеграция.Ошибки_Добавить(Ошибки, "Ошибка");
// ✅ Хорошо — контракт выражен явно, без зависимостей
Ошибки = Новый Структура("Отказ, Описания", Истина, Новый Массив);
// ❌ Плохо — зависимость от фабричного метода другого модуля
ПараметрыПреобразования = ВнешняяСистемаИнтеграция.ПараметрыПреобразования_Инициализация("date");
// ✅ Хорошо — структура видна, намерение очевидно
МассивИмен = Новый Массив;
МассивИмен.Добавить("date");
ПараметрыПреобразования = Новый Структура("ИменаСвойствСоЗначениямиДата", МассивИмен);
Если одна и та же структура данных нужна в нескольких тестах — выноси построение в приватный helper тестового модуля (СлужебныеПроцедурыИФункции), не копируй код.
Делай helper параметрическим конструктором: принимает только значимые для сценария данные, возвращает полностью инициализированный объект. Шаблонный «скучный» код (создание структуры, заглушки для необязательных полей) остаётся внутри helper — тест видит только то, что важно для конкретной проверки. Необязательные части — через параметры по умолчанию:
// Конструктор структуры с переменными полями
Функция СоздатьДвижение(Период, ТипЦен, Номенклатура, Организация, Цена)
Результат = Новый Структура();
Результат.Вставить("Период", Период);
Результат.Вставить("ТипЦен", ТипЦен);
Результат.Вставить("Номенклатура", Номенклатура);
Результат.Вставить("Организация", Организация);
Результат.Вставить("Цена", Цена);
Возврат Результат;
КонецФункции
// Конструктор с опциональными параметрами
Функция СоздатьГруппу(Наименование, Родитель = Неопределено, ПометкаУдаления = Ложь)
Возврат ЮТест.Данные().КонструкторОбъекта("Справочник.Номенклатура")
.Установить("Наименование", Наименование)
.Установить("Родитель", Родитель)
.Установить("ЭтоГруппа", Истина)
.Установить("ПометкаУдаления", ПометкаУдаления)
.Записать( , Истина);
КонецФункции
// Конструктор мок-ответа внешнего сервиса — удобно при мокировании HTTP
Функция ОтветВнешнегоСервиса(Знач Код, Знач Метод, Знач Тело, Знач Ошибки = Неопределено)
Если Ошибки = Неопределено Тогда
Ошибки = Новый Массив();
КонецЕсли;
Возврат Новый Структура(
"КодСостояния, URL, Метод, Тело, Ошибки",
Код, "", Метод, Тело, Ошибки);
КонецФункции
Строитель коллекции (создание + добавление строк):
Функция ОписаниеТоваров()
Результат = Новый ТаблицаЗначений();
Результат.Колонки.Добавить("Код");
Результат.Колонки.Добавить("Ссылка");
Возврат Результат;
КонецФункции
Процедура ДобавитьОписаниеТовара(ОписаниеТоваров, Код, Ссылка)
Строка = ОписаниеТоваров.Добавить();
Строка.Код = Код;
Строка.Ссылка = Ссылка;
КонецПроцедуры
Массив-список значений — через ЮТКоллекции.ЗначениеВМассиве. Когда коллекция это просто фиксированный перечень значений, явную сборку (Новый Массив() + серия .Добавить()) заменяй на однострочный helper фреймворка. Принцип «строй данные напрямую» не нарушается — ЮТКоллекции часть YAxUnit, а не тестируемого кода, — но код короче и читаемее:
// ❌ Многословно — три строки на список из одного значения
Объекты = Новый Массив();
Объекты.Добавить(Контрагент1);
// ✅ Одна строка; метод принимает до 10 значений
Объекты = ЮТКоллекции.ЗначениеВМассиве(Контрагент1);
Объекты = ЮТКоллекции.ЗначениеВМассиве(Контрагент1, Контрагент2);
Данные «для запроса» — генерируй inline, не выноси в методы, если они не являются предметом теста. В интеграционных тестах часть данных нужна только чтобы отработал запрос тестируемого метода (например, обязательное ВНУТРЕННЕЕ СОЕДИНЕНИЕ с курсом валюты), но на логику метода и на проверки не влияет. Для такой обвязки:
- не заводи отдельные helper-методы (
СоздатьВалюту, СоздатьКурс) — это шум; строй объект прямо в // given;
- не задавай конкретные значения через
Установить там, где значение не важно — используй генераторы ФикцияОбязательныхПолей(), Фикция("Реквизит"). Установить оставляй только для полей-связок, от которых зависит попадание строки в результат (ключи соединения, измерения из отбора, период среза).
// given
// Валюта и курс нужны только чтобы прайс прошел ВНУТРЕННЕЕ СОЕДИНЕНИЕ с КурсыВалют
Валюта = ЮТест.Данные().КонструкторОбъекта("Справочник.Валюты").ФикцияОбязательныхПолей().Записать();
ЮТест.Данные().КонструкторОбъекта("РегистрСведений.КурсыВалют")
.Установить("Период", НачалоДня(ТекущаяДатаСеанса())) // важно: попасть в срез
.Установить("Валюта", Валюта) // важно: ключ соединения
.Фикция("Курс") // не важно
.Фикция("Кратность") // не важно
.ДобавитьЗапись();
Именованный helper оправдан для данных, которые являются предметом теста (их поля осмысленны для сценария) — не для чистой запросной обвязки.
Атомарность и независимость тестов
Каждый тест должен быть самодостаточным: он сам создаёт всё необходимое и не зависит ни от других тестов, ни от порядка их выполнения.
Данные — в самом тесте (или в его хуке ПередКаждымТестом):
Процедура ПоискНаходитСозданныйЭлемент() Экспорт
// given — каждый тест создаёт свои данные
Номенклатура = СоздатьНоменклатуру("Тестовый товар");
// when
Результат = МойМодуль.НайтиПоНаименованию("Тестовый товар");
// then
ЮТест.ОжидаетЧто(Результат).Равно(Номенклатура);
КонецПроцедуры
Общие данные допустимы только для неизменяемых объектов. Если данные, созданные в ПередВсемиТестами или ПередТестовымНабором, могут быть изменены хотя бы одним тестом — это хрупкая конструкция: тест, запущенный после модифицирующего, получит другое состояние и даст ложный результат. Общие данные — только для справочных/статичных объектов, которые тесты читают, но не меняют.
| Ситуация | Где создавать данные |
|---|
| Данные уникальны для теста | в теле теста или ПередКаждымТестом + .ВТранзакции() |
| Тяжёлый неизменяемый объект для всего набора | ПередТестовымНабором + .УдалениеТестовыхДанных() |
Изоляция данных через .ВТранзакции() — основной инструмент: каждый тест получает чистое состояние БД без ручной уборки.
Что проверять
Граничные условия — пустые коллекции, нулевые значения, минимум/максимум диапазона, пустая строка вместо заполненной.
Обработку ошибок — если метод выбрасывает исключение при нарушении предусловий, это тоже поведение, которое нужно покрыть:
ЮТест.ОжидаетЧто(МойМодуль)
.Метод("МойМетод", ЮТКоллекции.ЗначениеВМассиве(НедопустимыйПараметр))
.ВыбрасываетИсключение();
Корректность данных — не только «вернул что-то», но и что именно: проверяй конкретные поля результата, а не только факт его наличия. Отдавай предпочтение проверкам через Предикаты (references/framework/predicates.md).
Производительность — как правило, вне зоны ответственности юнит-теста; не добавляй проверки производительности без явной задачи.
Проверка ранних выходов через spy, а не через состояние
У метода с ранними выходами (guard-условия: выключена функциональная опция, пустой вход, нет связанных данных) смысл ветки один — побочный эффект не наступил. Такую ветку можно проверять двумя способами:
- State-based — предварительно записать данные, вызвать метод, убедиться, что данные не изменились;
- Interaction-based (spy) — понаблюдать за мутирующим методом-коллаборатором и убедиться, что он не вызывался.
Для ранних выходов spy обычно точнее и дешевле. Он прямо выражает контракт ветки («до мутации не дошли»), а не выводит его косвенно из неизменности данных. Плюс убирает фикстуры и чтение БД: setup-записи и блок СодержитЗаписи не нужны, а значит такому тесту не нужна и .ВТранзакции().
Отдельный сильный аргумент — отсутствие дублирования покрытия. Если СодержитЗаписи перепроверяет, что именно мутирующий метод делает с записями, а этот метод уже покрыт своим тестом (напр. тест модуля менеджера регистра), то state-проверка в тесте-оркестраторе дублирует чужое покрытие. Задача теста-оркестратора — ветвление, а не данные коллаборатора.
Как: Мокито.Наблюдать(...) в фазе обучения + Мокито.Проверить(...).КоличествоВызовов(...).Равно(0) в // then. Наблюдение, как и мок, работает через &Вместо — для наблюдаемого метода нужен перехватчик (references/interceptors.md). При отсутствии обучения перехватчик уходит в ПродолжитьВызов — реальный метод, поэтому добавление перехватчика не ломает интеграционные тесты того же метода.
Каждый тест должен доказывать свою ветку. Если несколько ранних выходов проверять только по «мутация не произошла», их проверки станут неотличимы. Чтобы тест доказывал именно свой guard, добавь наблюдение за коллаборатором следующего шага:
// Ветка 1: функциональная опция выключена -> выход на первом же условии,
// заказы даже не запрашивались
Мокито.Обучение(МодульВызова)
.Когда("ОбрабатыватьГрузовыеМеста").Вернуть(Ложь)
.Обучение(МодульСервер)
.Наблюдать("ЗаказПоставщику")
.Обучение(РегистрыСведений.ГрузовыеМеста)
.Наблюдать("УстановитьПризнакСканирования")
.Прогон();
// when
МодульВызова.СброситьПризнакНеполногоСканирования(Неопределено, ТестОтказ);
// then
Мокито.Проверить(МодульСервер)
.КоличествоВызовов("ЗаказПоставщику").Равно(0); // выход ДО запроса заказов
Мокито.Проверить(РегистрыСведений.ГрузовыеМеста)
.КоличествоВызовов("УстановитьПризнакСканирования").Равно(0); // мутации не было
// Ветка 2: заказы запрошены, но пусты -> выход на втором условии
Мокито.Обучение(МодульВызова)
.Когда("ОбрабатыватьГрузовыеМеста").Вернуть(Истина)
.Обучение(МодульСервер)
.Когда("ЗаказПоставщику").Вернуть(Новый Массив)
.Обучение(РегистрыСведений.ГрузовыеМеста)
.Наблюдать("УстановитьПризнакСканирования")
.Прогон();
// ... then: КоличествоВызовов("УстановитьПризнакСканирования").Равно(0)
Компромисс — назови его. Spy жёстче связан с реализацией: тест фиксирует, что мутация идёт именно через конкретный метод-коллаборатор. Если сброс перепишут на другой механизм (напр. прямую запись набора), spy-тест придётся поправить, хотя поведение осталось корректным. Для веток «метод ничего не делает» такая связанность приемлема и даже уместна. Но если ветка производит осмысленное состояние (что-то записала, посчитала, изменила поля) — проверяй состояние (state-based) и предикаты по данным, а не факт вызова.
Тестирование методов с ПолучитьФункциональнуюОпцию()
Если тестируемый метод внутри вызывает ПолучитьФункциональнуюОпцию(), нельзя использовать .ВТранзакции() для изоляции данных. Причина: платформа 1С читает значение функциональной опции из внетранзакционного кеша — изменение константы внутри открытой транзакции невидимо для ПолучитьФункциональнуюОпцию(), и тест всегда получит старое значение.
Правила:
- Без транзакции — тест устанавливает константу напрямую, без
.ВТранзакции().
- Последнее значение —
Ложь — если тест параметрический (булевая константа), последним ставь параметр Ложь. Это булево значение по умолчанию в 1С, и после прогона тестов константа окажется в «выключенном» состоянии, что снизит риск влияния на другие тесты, использующие эту же функциональную опцию.
// ИсполняемыеСценарии — без .ВТранзакции()
.ДобавитьТест("МетодВозвращаетЗначениеФункциональнойОпции")
.СПараметрами(Истина)
.СПараметрами(Ложь) // ← последний: константа остаётся Ложь после теста
// Реализация
Процедура МетодВозвращаетЗначениеФункциональнойОпции(ЗначениеОпции) Экспорт
// given
Константы.МояКонстанта.Установить(ЗначениеОпции);
// when
Результат = МойМодуль.МойМетод();
// then
ЮТест.ОжидаетЧто(Результат).Равно(ЗначениеОпции);
КонецПроцедуры
Исключение — ФО читается через метод-обёртку. Если тестируемый код узнаёт значение ФО не напрямую, а через метод конфигурации-обёртку (напр. Модуль.ОбрабатыватьХ() = ПолучитьФункциональнуюОпцию("...")), правило выше можно обойти: замокировать саму обёртку (Мокито.Обучение(Модуль).Когда("ОбрабатыватьХ").Вернуть(Истина/Ложь)). Тогда ПолучитьФункциональнуюОпцию() не вызывается, внетранзакционного кеша ФО в игре нет — и тест можно держать в .ВТранзакции() (чистый откат вместо ручной чистки данных). Мок ловит и внутримодульный вызов обёртки из тестируемого метода того же модуля.
Ограничение: работает только через метод-обёртку. Прямой вызов ПолучитьФункциональнуюОпцию() внутри тестируемого метода замокировать нельзя — это платформенный метод (см. ограничения Мокито в references/framework/mocking.md). Требует перехватчик &Вместо — см. references/interceptors.md.
Документируй намерение, а не механику
Комментарии // given / // when / // then сами по себе структурируют тест. Дополнительный комментарий нужен только если неочевидно зачем — почему именно эти данные, почему именно этот результат ожидается:
// given
// Цена = 0 — граничный кейс: при нулевой цене расчёт должен вернуть Неопределено, а не 0
Товар = СоздатьТовар();
ЮТест.Данные().УстановитьЗначениеРеквизита(Товар, "Цена", 0);
// when
Результат = Ценообразование.РассчитатьЦену(Товар);
// then
ЮТест.ОжидаетЧто(Результат).НеЗаполнено();
Не описывай то, что читается из кода — описывай причину выбора конкретного сценария.
Checklist при создании нового теста