Skill v1.0.1
Automated scan100/100+3 new
version: "1.0.1" name: test-writing description: "Use for написания тестовых модулей YaxUnit (BSL). Covers регистрация тестов, утверждения, мокирование и подготовку тестовых данных."
Написание тестов YaxUnit (BSL)
Запуск написанных тестов — отдельный навык `test-execution`.
Полная документация YaxUnit: см. `references/yaxunit-cheatsheet.md`
Расположение тестов в проекте
Тесты хранятся в отдельном расширении конфигурации: <корень проекта>/exts/TESTS/
Варианты структуры: EDT и DESIGNER
| Формат | Код модуля | Файл метаданных | |
|---|---|---|---|
| EDT | exts/TESTS/src/CommonModules/<ИмяМодуля>/Module.bsl | .../<ИмяМодуля>.mdo | |
| DESIGNER | exts/TESTS/src/CommonModules/<ИмяМодуля>/Ext/Module.bsl | .../<ИмяМодуля>.xml |
Если формат неочевиден — проверь application-*.yml / yaxunit-*.yml в корне проекта.
Не смешивай структуры: для DESIGNER обязателен Ext/, для EDT он не используется.
Новый модуль должен быть зарегистрирован в Configuration.[mdo|xml], иначе тест не подхватится раннером.
Категорические запреты:
- Тестовые исходники размещаются только в
exts/TESTS/**, никогда в основной конфигурации exts/YAXUNIT/**никогда не изменяется вручную — это инфраструктура раннера
Именование тестовых модулей
Шаблон: <Префикс>_<ИмяОбъекта>[_<Суффикс>]
Префиксы по типу объекта
| Тип объекта | Префикс | Пример | |
|---|---|---|---|
| Общий модуль | ОМ_ | ОМ_ОбщегоНазначения | |
| Документ | Док_ | Док_ПоступлениеТоваров | |
| Справочник | Спр_ | Спр_Контрагенты | |
| Регистр накопления | РН_ | РН_ОстаткиТоваров | |
| Регистр сведений | РС_ | РС_КурсыВалют | |
| Обработка | Обр_ | Обр_ЗакрытиеМесяца |
Суффиксы по типу модуля
| Тип модуля | Суффикс | Пример | |
|---|---|---|---|
| Модуль объекта | _МО | Спр_Контрагенты_МО | |
| Модуль менеджера | _ММ | РН_ОстаткиТоваров_ММ | |
| Модуль набора записей | _НЗ | РБ_Хозрасчетный_НЗ |
Структура тестового модуля
Обязательно: экспортная процедура ИсполняемыеСценарии. Только регистрация тестов — никаких данных, никакой логики.
Процедура ИсполняемыеСценарии() ЭкспортЮТТесты.ДобавитьТестовыйНабор("Остатки").ДобавитьСерверныйТест("ТестПолучитьОстатки").ДобавитьСерверныйТест("ТестОстатокПустойСклад").ДобавитьТестовыйНабор("Перемещение").ДобавитьСерверныйТест("ТестПеремещениеМеждуСкладами");КонецПроцедуры
| Метод | Где выполняется | |
|---|---|---|
ДобавитьТест | контекст по умолчанию | |
ДобавитьСерверныйТест | &НаСервереБезКонтекста | |
ДобавитьКлиентскийТест | &НаКлиенте |
Реализация теста
Один тест проверяет одно утверждение. Паттерн Arrange-Act-Assert:
Процедура ТестПолучитьОстатки() Экспорт// ArrangeСклад = ЮТест.Данные().СоздатьЭлемент("Справочник.Склады");НоменклатураСсылка = ЮТест.Данные().СоздатьЭлемент("Справочник.Номенклатура");// ActОстаток = УправлениеСкладом.ПолучитьОстаток(НоменклатураСсылка, Склад);// AssertЮТест.ОжидаетЧто(Остаток).Равно(0);КонецПроцедуры
Утверждения (ЮТест.ОжидаетЧто)
// Базовые сравненияЮТест.ОжидаетЧто(Результат).Равно(42);ЮТест.ОжидаетЧто(Результат).НеРавно(0);ЮТест.ОжидаетЧто(Результат).Больше(10);ЮТест.ОжидаетЧто(Флаг).ЭтоИстина();ЮТест.ОжидаетЧто(Значение).ВСписке(МассивДопустимых);// Тип и заполненностьЮТест.ОжидаетЧто(Ссылка).ИмеетТип("СправочникСсылка.Номенклатура");ЮТест.ОжидаетЧто(Значение).НеЯвляетсяНеопределено();// ИсключенияЮТест.ОжидаетЧто(ЭтотОбъект).МетодВыбрасываетИсключение("МетодСОшибкой", Параметры);// Данные ИБЮТест.ОжидаетЧтоТаблицаБазы("Справочник.Склады").СодержитЗаписи().ГдеРеквизит("Наименование").Равно("Основной склад");
Тестовые данные (ЮТест.Данные)
// ПустышкаСклад = ЮТест.Данные().СоздатьЭлемент("Справочник.Склады");// Конструктор с реквизитамиНоменклатура = ЮТест.Данные().КонструкторОбъекта("Справочник.Номенклатура").Установить("Наименование", "Тестовый товар").Установить("ЕдиницаИзмерения", ПредопределённыйЭлемент("Классификатор.ЕдиницыИзмерения.Штука")).Записать().Ссылка();// ДокументДокумент = ЮТест.Данные().СоздатьДокумент("Документ.ПоступлениеТоваров");
Данные через ЮТест.Данные() автоматически удаляются после теста. Не создавай данные в ИсполняемыеСценарии.
Мокирование (Мокито)
Паттерн: Обучение -> Прогон -> Проверка.
Процедура ТестРасчётСкидки() ЭкспортМокито.Обучение(МодульСкидок).Когда().ПолучитьПроцентСкидки(Клиент).Вернуть(15);Результат = УправлениеПродажами.РассчитатьСумму(100, Клиент);ЮТест.ОжидаетЧто(Результат).Равно(85);Мокито.Проверить(МодульСкидок).ПолучитьПроцентСкидки(Клиент);КонецПроцедуры
Сценарии мокирования
Мокито.Обучение(Модуль).Когда().МетодА(Параметр).Вернуть(42);Мокито.Обучение(Модуль).Когда().МетодБ(Параметр).ВыброситьИсключение("Текст ошибки");Мокито.Обучение(Модуль).Когда().МетодВ().Пропустить();Мокито.Обучение(Модуль).Когда().МетодГ().Наблюдать();
Жизненный цикл и обработчики событий
Процедура ИсполняемыеСценарии() ЭкспортЮТТесты.ДобавитьТестовыйНабор("Расчёты").Перед("ПередНаборомРасчёты").После("ПослеКаждогоТестаОчистка").ДобавитьСерверныйТест("ТестРасчётА").ДобавитьСерверныйТест("ТестРасчётБ");КонецПроцедурыПроцедура ПередНаборомРасчёты() ЭкспортЮТест.Контекст().УстановитьЗначение("Ставка", 18);КонецПроцедуры
Контекст — передача данных между методами
ЮТест.Контекст().УстановитьЗначение("МоёЗначение", Данные);Данные = ЮТест.Контекст().Значение("МоёЗначение");
Параметризованные тесты
Процедура ИсполняемыеСценарии() ЭкспортВарианты = ЮТест.Варианты().Добавить(0, "Нулевое количество", 0).Добавить(10, "Положительное", 100).Добавить(-5, "Отрицательное", 0);ЮТТесты.ДобавитьТестовыйНабор("Расчёт суммы").ДобавитьСерверныйТест("ТестРасчётСуммы").СПараметрами(Варианты);КонецПроцедурыПроцедура ТестРасчётСуммы(Количество, Описание, ОжидаемаяСумма) ЭкспортРезультат = МойМодуль.РассчитатьСумму(Количество);ЮТест.ОжидаетЧто(Результат).НазваниеПроверки(Описание).Равно(ОжидаемаяСумма);КонецПроцедуры
Изоляция тестовых данных (MUST)
Тест, пишущий в БД, обязан откатывать свои изменения. Без изоляции каждый прогон оставляет мусор в базе, тесты теряют идемпотентность.
Транзакционная изоляция через .ВТранзакции()
Fluent-метод .ВТранзакции() вызывается сразу после ДобавитьТестовыйНабор() — настройка применяется на уровне набора (рантайм ищет по иерархии: Тест → Набор → Модуль). Перед каждым тестом набора YaxUnit открывает транзакцию, после теста — откатывает её.
Процедура ИсполняемыеСценарии() ЭкспортЮТТесты.ДобавитьТестовыйНабор("Проведение документа").ВТранзакции() // ← изоляция: откат после каждого теста.ДобавитьСерверныйТест("ТестПроведениеСДоговором").ДобавитьСерверныйТест("ТестПроведениеСКорректнойСуммой");КонецПроцедуры
Справочники — только через ЮТест.Данные()
Элементы справочников создавать через ЮТест.Данные().СоздатьЭлемент(...) или КонструкторОбъекта(...).Записать(). Такие объекты трекаются YaxUnit и удаляются автоматически. Прямой вызов Справочники.X.СоздатьЭлемент() — антипаттерн: объект не трекается, остаётся в базе.
Документы через СоздатьДокумент() — обязательный teardown
ЮТест.Данные().СоздатьДокумент(...) трекается и удаляется автоматически. Но если документ создаётся через Документы.X.СоздатьДокумент() напрямую — он НЕ трекается, нужен явный teardown в .После("ИмяПроцедурыОчистки").
Исключения из .ВТранзакции()
Три ситуации, когда .ВТранзакции() применять НЕЛЬЗЯ — при каждой обязателен комментарий-обоснование у набора и teardown через .После():
| Ситуация | Причина исключения | Способ изоляции | |
|---|---|---|---|
(а) Негативный тест проведения (ожидаемый Отказ) | Упавшая вложенная транзакция отравляет внешнюю: «В данной транзакции уже происходили ошибки!» при последующих чтениях | .УдалениеТестовыхДанных() + .После("Очистка") | |
| (б) Прод-код с гвардом `ТранзакцияАктивна()` | Двухфазные фиксации, реальные API-вызовы, регистры с уникальным ключом — падают или ведут себя непредсказуемо внутри транзакции | .После("Очистка") с ручной зачисткой | |
| (в) Клиентский контекст | ДобавитьКлиентскийТест — транзакционный откат на клиенте недоступен по архитектуре платформы | Перед/После-обработчики с серверным контекстом |
// Исключение (а): негативный тест — ожидаемый Отказ отравляет внешнюю транзакцию.// Изоляция: ЮТест.Данные() + .УдалениеТестовыхДанных() + teardown в .После().ЮТТесты.ДобавитьТестовыйНабор("Запрет проведения").УдалениеТестовыхДанных().После("ОчиститьДокументыЗапретПроведения").ДобавитьСерверныйТест("ТестЗапретБезДоговора");
Паттерн перечитывания объекта при перепроведении
Тест, меняющий режим записи документа, обязан перечитывать объект между сменами — это моделирует поведение формы:
// ПровестиДокОбъект = ДокСсылка.ПолучитьОбъект();ДокОбъект.Записать(РежимЗаписиДокумента.Проведение);// Отменить проведение — перечитываем, как формаДокОбъект = ДокСсылка.ПолучитьОбъект();ДокОбъект.Записать(РежимЗаписиДокумента.ОтменаПроведения);
Ограничение платформы 8.3.27: программное перепроведение в серверной сессии иногда даёт [ОшибкаХранимыхДанных] — стек без прикладных кадров, ни перечитывание, ни .ВТранзакции() не помогают. Идемпотентность перепроведения в таком случае проверяется сценарным слоем (Vanessa), юнит-тест оформляется через ЮТест.Пропустить() с явным обоснованием.
Чеклист самоочистки — проверка запросами (MUST после написания/правки пишущих тестов)
Изоляция декларируется в коде (.ВТранзакции(), teardown), а доказывается фактом — счётчиками в БД до/после прогона. Зелёный прогон НЕ доказывает чистоту: тест может пройти и оставить мусор.
- Карта таблиц. Составь список таблиц, в которые тесты модуля могут писать: всё, что создают
Перед-хендлеры и тела тестов (справочники, документы, регистры), включая объекты контекстных хелперов (СоздатьКонтекстТестаи т.п.). - Счётчики ДО. Для каждой таблицы —
ВЫБРАТЬ КОЛИЧЕСТВО(*) ИЗ Справочник.X(платформенный запрос: MCPexecute_query/ консоль запросов). Для регистров сведений — счётчик по измерению-маркеру теста, не по всей таблице. - Прогон + счётчики ПОСЛЕ. Прогнать модуль целиком, снять те же счётчики. Дельта по каждой таблице = 0.
- Второй прогон. Повторить: GREEN + дельта снова 0. Первый прогон мог «съесть» чужой накопленный мусор и замаскировать собственный — повторяемость обязательна.
- Дельта ≠ 0 → виновник. Найти оставшиеся объекты по именам/маркерам (
ВЫБРАТЬ Наименование ... ГДЕ Наименование ПОДОБНО "...%"), определить создающий хендлер/тест, добавить teardown, повторить чеклист с шага 2.
Ловушки (прецедент TASK-173 / TASK-165.7):
| Ловушка | Суть | |
|---|---|---|
Авто-удаление YaxUnit падает на .Записать(Ложь, Истина) | УстановитьПометкуУдаления повторно валидирует обязательные реквизиты и отказывает — объект остаётся. Teardown — физическое удаление: Объект.ОбменДанными.Загрузка = Истина; Объект.Удалить(); | |
| Частичный teardown | После-хендлер чистит только часть созданного (например семафор-регистр, но не аккаунт-справочник) — выглядит как teardown, но мусорит | |
| «GREEN = чисто» | 41/41 тестов зелёные, при этом один модуль оставлял +15 объектов за прогон — обнаружено только счётчиками |
Антипаттерны
| Антипаттерн | Правильно | |
|---|---|---|
Данные в ИсполняемыеСценарии | Данные в теле теста или Перед-обработчике | |
| Один тест проверяет 10 условий | Один тест — одно утверждение | |
| Тест зависит от порядка выполнения | Каждый тест изолирован | |
| Хардкод ссылок на объекты ИБ | Создавать через ЮТест.Данные() | |
| Тестировать приватную логику | Тестировать через публичный интерфейс | |
| Мокировать тестируемый модуль | Мокировать только зависимости | |
Пишущий набор без .ВТранзакции() | .ВТранзакции() по умолчанию; исключения — с комментарием + teardown | |
Справочники.X.СоздатьЭлемент() в тесте | ЮТест.Данные().СоздатьЭлемент() — трекается и удаляется автоматически | |
.ВТранзакции() на негативном тесте проведения | Исключение (а) — отравляет транзакцию; использовать .УдалениеТестовыхДанных() + .После() | |
| «GREEN = чисто» — приёмка без счётчиков | Чеклист самоочистки: счётчики до/после запросами, два прогона, дельта 0 |
Слои и фазы TDD, границы ролей
Тесты и реализация пишутся разными агентами в разных фазах. Автор тестов не знает реализацию, автор кода не модифицирует тесты.
Phase 3a: Scenario-Author → .feature (BDD) ┐ параллельноPhase 3b: Developer-Tests → unit-тесты (Red) ┘Phase 3c: Developer-Code → код (Green)Phase 4: Tester → edge cases, регрессия, BDD + unit
Слои тестирования
| Слой | Фаза | Агент | Покрывает | |
|---|---|---|---|---|
| BDD (acceptance) | 3a | Scenario-Author | Поведение через UI | |
| TDD (unit) | 3b | Developer-Tests | Публичные методы, MUST-сценарии, базовые негативы | |
| TDD (green) | 3c | Developer-Code | Реализация, проходящая unit-тесты | |
| Coverage | 4 | Tester | Edge cases, интеграция, регрессия |
Phase 3a и 3b — параллельно. Phase 3c стартует после завершения обоих.
Границы агентов
- Scenario-Author: НЕ пишет unit-тесты, НЕ запускает сценарии, НЕ расширяет за пределы спецификации
- Developer-Tests: MUST-сценарии + базовые негативы; НЕ покрывает комбинаторные edge cases и интеграцию
- Tester: дополняет покрытие; НЕ дублирует тесты Developer; НЕ правит BSL-код
Правило при падении теста у Tester
Тест упал├── Ошибка в тесте → Tester исправляет (test_error)└── Баг в коде → СТОП. Метка implementation_error + описание.Оркестратор возвращает задачу Developer.
User/Role context в Test Plan: если код использует SetPrivilegedMode, проверки ролей (AccessRight, RoleAvailable) или результат зависит от текущего пользователя — спецификация ДОЛЖНА в разделе «Test Plan» явно указать для каждого теста: имя пользователя/набор ролей, требуемый режим (привилегированный или нет), ожидаемый результат (успех/отказ). Без этого тест под full-rights runner-ом (например AgentAI) даст false-positive: пройдёт «по совпадению» через привилегированную ветку, не проверив роле-зависимое поведение. Если для unit это технически невозможно — фиксируется в спеке отдельным ADR с переносом в integration scope (Phase 4).
depends_on: []