framework/skills/bsl-practices/test-writing/SKILL.md
Use for написания тестовых модулей YaxUnit (BSL). Covers регистрация тестов, утверждения, мокирование и подготовку тестовых данных.
npx skillsauth add steelmorgan/1c-agent-based-dev-framework test-writingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Запуск написанных тестов — отдельный навык test-execution.
Полная документация YaxUnit: см. references/yaxunit-cheatsheet.md
Тесты хранятся в отдельном расширении конфигурации: <корень проекта>/exts/TESTS/
| Формат | Код модуля | Файл метаданных |
|--------|------------|-----------------|
| 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);
ЮТТесты
.ДобавитьТестовыйНабор("Расчёт суммы")
.ДобавитьСерверныйТест("ТестРасчётСуммы")
.СПараметрами(Варианты);
КонецПроцедуры
Процедура ТестРасчётСуммы(Количество, Описание, ОжидаемаяСумма) Экспорт
Результат = МойМодуль.РассчитатьСумму(Количество);
ЮТест.ОжидаетЧто(Результат)
.НазваниеПроверки(Описание)
.Равно(ОжидаемаяСумма);
КонецПроцедуры
Тест, пишущий в БД, обязан откатывать свои изменения. Без изоляции каждый прогон оставляет мусор в базе, тесты теряют идемпотентность.
.ВТранзакции()Fluent-метод .ВТранзакции() вызывается сразу после ДобавитьТестовыйНабор() — настройка применяется на уровне набора (рантайм ищет по иерархии: Тест → Набор → Модуль). Перед каждым тестом набора YaxUnit открывает транзакцию, после теста — откатывает её.
Процедура ИсполняемыеСценарии() Экспорт
ЮТТесты
.ДобавитьТестовыйНабор("Проведение документа")
.ВТранзакции() // ← изоляция: откат после каждого теста
.ДобавитьСерверныйТест("ТестПроведениеСДоговором")
.ДобавитьСерверныйТест("ТестПроведениеСКорректнойСуммой");
КонецПроцедуры
ЮТест.Данные()Элементы справочников создавать через ЮТест.Данные().СоздатьЭлемент(...) или КонструкторОбъекта(...).Записать(). Такие объекты трекаются YaxUnit и удаляются автоматически. Прямой вызов Справочники.X.СоздатьЭлемент() — антипаттерн: объект не трекается, остаётся в базе.
СоздатьДокумент() — обязательный teardownЮТест.Данные().СоздатьДокумент(...) трекается и удаляется автоматически. Но если документ создаётся через Документы.X.СоздатьДокумент() напрямую — он НЕ трекается, нужен явный teardown в .После("ИмяПроцедурыОчистки").
.ВТранзакции()Три ситуации, когда .ВТранзакции() применять НЕЛЬЗЯ — при каждой обязателен комментарий-обоснование у набора и teardown через .После():
| Ситуация | Причина исключения | Способ изоляции |
|---|---|---|
| (а) Негативный тест проведения (ожидаемый Отказ) | Упавшая вложенная транзакция отравляет внешнюю: «В данной транзакции уже происходили ошибки!» при последующих чтениях | .УдалениеТестовыхДанных() + .После("Очистка") |
| (б) Прод-код с гвардом ТранзакцияАктивна() | Двухфазные фиксации, реальные API-вызовы, регистры с уникальным ключом — падают или ведут себя непредсказуемо внутри транзакции | .После("Очистка") с ручной зачисткой |
| (в) Клиентский контекст | ДобавитьКлиентскийТест — транзакционный откат на клиенте недоступен по архитектуре платформы | Перед/После-обработчики с серверным контекстом |
// Исключение (а): негативный тест — ожидаемый Отказ отравляет внешнюю транзакцию.
// Изоляция: ЮТест.Данные() + .УдалениеТестовыхДанных() + teardown в .После().
ЮТТесты
.ДобавитьТестовыйНабор("Запрет проведения")
.УдалениеТестовыхДанных()
.После("ОчиститьДокументыЗапретПроведения")
.ДобавитьСерверныйТест("ТестЗапретБезДоговора");
Тест, меняющий режим записи документа, обязан перечитывать объект между сменами — это моделирует поведение формы:
// Провести
ДокОбъект = ДокСсылка.ПолучитьОбъект();
ДокОбъект.Записать(РежимЗаписиДокумента.Проведение);
// Отменить проведение — перечитываем, как форма
ДокОбъект = ДокСсылка.ПолучитьОбъект();
ДокОбъект.Записать(РежимЗаписиДокумента.ОтменаПроведения);
Ограничение платформы 8.3.27: программное перепроведение в серверной сессии иногда даёт [ОшибкаХранимыхДанных] — стек без прикладных кадров, ни перечитывание, ни .ВТранзакции() не помогают. Идемпотентность перепроведения в таком случае проверяется сценарным слоем (Vanessa), юнит-тест оформляется через ЮТест.Пропустить() с явным обоснованием.
Изоляция декларируется в коде (.ВТранзакции(), teardown), а доказывается фактом — счётчиками в БД до/после прогона. Зелёный прогон НЕ доказывает чистоту: тест может пройти и оставить мусор.
Перед-хендлеры и тела тестов (справочники, документы, регистры), включая объекты контекстных хелперов (СоздатьКонтекстТеста и т.п.).ВЫБРАТЬ КОЛИЧЕСТВО(*) ИЗ Справочник.X (платформенный запрос: MCP execute_query / консоль запросов). Для регистров сведений — счётчик по измерению-маркеру теста, не по всей таблице.ВЫБРАТЬ Наименование ... ГДЕ Наименование ПОДОБНО "...%"), определить создающий хендлер/тест, добавить teardown, повторить чеклист с шага 2.Ловушки (прецедент TASK-173 / TASK-165.7):
| Ловушка | Суть |
|---|---|
| Авто-удаление YaxUnit падает на .Записать(Ложь, Истина) | УстановитьПометкуУдаления повторно валидирует обязательные реквизиты и отказывает — объект остаётся. Teardown — физическое удаление: Объект.ОбменДанными.Загрузка = Истина; Объект.Удалить(); |
| Частичный teardown | После-хендлер чистит только часть созданного (например семафор-регистр, но не аккаунт-справочник) — выглядит как teardown, но мусорит |
| «GREEN = чисто» | 41/41 тестов зелёные, при этом один модуль оставлял +15 объектов за прогон — обнаружено только счётчиками |
| Антипаттерн | Правильно |
|-------------|-----------|
| Данные в ИсполняемыеСценарии | Данные в теле теста или Перед-обработчике |
| Один тест проверяет 10 условий | Один тест — одно утверждение |
| Тест зависит от порядка выполнения | Каждый тест изолирован |
| Хардкод ссылок на объекты ИБ | Создавать через ЮТест.Данные() |
| Тестировать приватную логику | Тестировать через публичный интерфейс |
| Мокировать тестируемый модуль | Мокировать только зависимости |
| Пишущий набор без .ВТранзакции() | .ВТранзакции() по умолчанию; исключения — с комментарием + teardown |
| Справочники.X.СоздатьЭлемент() в тесте | ЮТест.Данные().СоздатьЭлемент() — трекается и удаляется автоматически |
| .ВТранзакции() на негативном тесте проведения | Исключение (а) — отравляет транзакцию; использовать .УдалениеТестовыхДанных() + .После() |
| «GREEN = чисто» — приёмка без счётчиков | Чеклист самоочистки: счётчики до/после запросами, два прогона, дельта 0 |
Тесты и реализация пишутся разными агентами в разных фазах. Автор тестов не знает реализацию, автор кода не модифицирует тесты.
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 стартует после завершения обоих.
Тест упал
├── Ошибка в тесте → 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).
testing
MUST use BEFORE making a judgment about the cause of a conflict, a test failure, or an artifact dispute. Defines the end-to-end verification method L1→L6 and the classification of the first broken link.
development
MUST use AFTER a work cycle with ≥2 iterations (wrote → error → fixed → success). Provides the retrospective procedure and the format for recording practice/anti-patterns in references/learned-patterns.md or {project}/.context/learned-patterns.md.
tools
MUST use WHEN you are writing reusable knowledge into RLM (pattern / architectural decision / stable domain fact) OR reading it before a non-trivial task/solution in the domain. Provides the breakdown of native-push vs RLM-pull, tools for writing and reading RLM, H-MEM levels, and hygiene.
testing
MUST use WHEN the task is classified as simple (< 20 lines, 1 file, no new metadata objects, no architectural decisions). Provides a short cycle of 3 steps with a guard on the self path and mandatory verify.