framework_eng/skills/bsl-practices/test-writing/SKILL.md
Use for writing YaxUnit (BSL) test modules. Covers test registration, assertions, mocking, and test data preparation.
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.
Running written tests is a separate skill test-execution.
Full YaxUnit documentation: see references/yaxunit-cheatsheet.md
Tests are stored in a separate configuration extension: <project root>/exts/TESTS/
| Format | Module code | Metadata file |
|--------|------------|-----------------|
| EDT | exts/TESTS/src/CommonModules/<ИмяМодуля>/Module.bsl | .../<ИмяМодуля>.mdo |
| DESIGNER | exts/TESTS/src/CommonModules/<ИмяМодуля>/Ext/Module.bsl | .../<ИмяМодуля>.xml |
If the format is not obvious, check application-*.yml / yaxunit-*.yml at the project root.
Do not mix the structures: Ext/ is required for DESIGNER, and it is not used for EDT.
A new module must be registered in Configuration.[mdo|xml], otherwise the runner will not pick up the test.
Absolute prohibitions:
exts/TESTS/**, never in the main configurationexts/YAXUNIT/** is never edited manually - this is runner infrastructureTemplate: <Prefix>_<ObjectName>[_<Suffix>]
| Object type | Prefix | Example |
|-------------|---------|--------|
| Общий модуль | ОМ_ | ОМ_ОбщегоНазначения |
| Документ | Док_ | Док_ПоступлениеТоваров |
| Справочник | Спр_ | Спр_Контрагенты |
| Регистр накопления | РН_ | РН_ОстаткиТоваров |
| Регистр сведений | РС_ | РС_КурсыВалют |
| Обработка | Обр_ | Обр_ЗакрытиеМесяца |
| Module type | Suffix | Example |
|-----------|---------|--------|
| Object module | _МО | Спр_Контрагенты_МО |
| Manager module | _ММ | РН_ОстаткиТоваров_ММ |
| Record set module | _НЗ | РБ_Хозрасчетный_НЗ |
Mandatory: export procedure ИсполняемыеСценарии. Registration only - no data, no logic.
Процедура ИсполняемыеСценарии() Экспорт
ЮТТесты
.ДобавитьТестовыйНабор("Остатки")
.ДобавитьСерверныйТест("ТестПолучитьОстатки")
.ДобавитьСерверныйТест("ТестОстатокПустойСклад")
.ДобавитьТестовыйНабор("Перемещение")
.ДобавитьСерверныйТест("ТестПеремещениеМеждуСкладами");
КонецПроцедуры
| Method | Where it runs |
|-------|----------------|
| ДобавитьТест | default context |
| ДобавитьСерверныйТест | &НаСервереБезКонтекста |
| ДобавитьКлиентскийТест | &НаКлиенте |
One test checks one assertion. Arrange-Act-Assert pattern:
Процедура ТестПолучитьОстатки() Экспорт
// Arrange
Склад = ЮТест.Данные().СоздатьЭлемент("Справочник.Склады");
НоменклатураСсылка = ЮТест.Данные().СоздатьЭлемент("Справочник.Номенклатура");
// Act
Остаток = УправлениеСкладом.ПолучитьОстаток(НоменклатураСсылка, Склад);
// Assert
ЮТест.ОжидаетЧто(Остаток).Равно(0);
КонецПроцедуры
// Базовые сравнения
ЮТест.ОжидаетЧто(Результат).Равно(42);
ЮТест.ОжидаетЧто(Результат).НеРавно(0);
ЮТест.ОжидаетЧто(Результат).Больше(10);
ЮТест.ОжидаетЧто(Флаг).ЭтоИстина();
ЮТест.ОжидаетЧто(Значение).ВСписке(МассивДопустимых);
// Тип и заполненность
ЮТест.ОжидаетЧто(Ссылка).ИмеетТип("СправочникСсылка.Номенклатура");
ЮТест.ОжидаетЧто(Значение).НеЯвляетсяНеопределено();
// Исключения
ЮТест.ОжидаетЧто(ЭтотОбъект).МетодВыбрасываетИсключение("МетодСОшибкой", Параметры);
// Данные ИБ
ЮТест.ОжидаетЧтоТаблицаБазы("Справочник.Склады")
.СодержитЗаписи()
.ГдеРеквизит("Наименование").Равно("Основной склад");
// Пустышка
Склад = ЮТест.Данные().СоздатьЭлемент("Справочник.Склады");
// Конструктор с реквизитами
Номенклатура = ЮТест.Данные()
.КонструкторОбъекта("Справочник.Номенклатура")
.Установить("Наименование", "Тестовый товар")
.Установить("ЕдиницаИзмерения", ПредопределённыйЭлемент("Классификатор.ЕдиницыИзмерения.Штука"))
.Записать()
.Ссылка();
// Документ
Документ = ЮТест.Данные().СоздатьДокумент("Документ.ПоступлениеТоваров");
Data created through ЮТест.Данные() is automatically deleted after the test. Do not create data in ИсполняемыеСценарии.
Pattern: Training -> Run -> Verify.
Процедура ТестРасчётСкидки() Экспорт
Мокито.Обучение(МодульСкидок)
.Когда().ПолучитьПроцентСкидки(Клиент)
.Вернуть(15);
Результат = УправлениеПродажами.РассчитатьСумму(100, Клиент);
ЮТест.ОжидаетЧто(Результат).Равно(85);
Мокито.Проверить(МодульСкидок).ПолучитьПроцентСкидки(Клиент);
КонецПроцедуры
Мокито.Обучение(Модуль).Когда().МетодА(Параметр).Вернуть(42);
Мокито.Обучение(Модуль).Когда().МетодБ(Параметр).ВыброситьИсключение("Текст ошибки");
Мокито.Обучение(Модуль).Когда().МетодВ().Пропустить();
Мокито.Обучение(Модуль).Когда().МетодГ().Наблюдать();
Процедура ИсполняемыеСценарии() Экспорт
ЮТТесты
.ДобавитьТестовыйНабор("Расчёты")
.Перед("ПередНаборомРасчёты")
.После("ПослеКаждогоТестаОчистка")
.ДобавитьСерверныйТест("ТестРасчётА")
.ДобавитьСерверныйТест("ТестРасчётБ");
КонецПроцедуры
Процедура ПередНаборомРасчёты() Экспорт
ЮТест.Контекст().УстановитьЗначение("Ставка", 18);
КонецПроцедуры
ЮТест.Контекст().УстановитьЗначение("МоёЗначение", Данные);
Данные = ЮТест.Контекст().Значение("МоёЗначение");
Процедура ИсполняемыеСценарии() Экспорт
Варианты = ЮТест.Варианты()
.Добавить(0, "Нулевое количество", 0)
.Добавить(10, "Положительное", 100)
.Добавить(-5, "Отрицательное", 0);
ЮТТесты
.ДобавитьТестовыйНабор("Расчёт суммы")
.ДобавитьСерверныйТест("ТестРасчётСуммы")
.СПараметрами(Варианты);
КонецПроцедуры
Процедура ТестРасчётСуммы(Количество, Описание, ОжидаемаяСумма) Экспорт
Результат = МойМодуль.РассчитатьСумму(Количество);
ЮТест.ОжидаетЧто(Результат)
.НазваниеПроверки(Описание)
.Равно(ОжидаемаяСумма);
КонецПроцедуры
A test that writes to the DB must roll back its changes. Without isolation, every run leaves garbage in the database and the tests lose idempotency.
.ВТранзакции()The fluent method .ВТранзакции() is called immediately after ДобавитьТестовыйНабор() - the setting applies at the set level (runtime resolves it by hierarchy: Test -> Set -> Module). Before each test in the set, YaxUnit opens a transaction, and after the test it rolls it back.
Процедура ИсполняемыеСценарии() Экспорт
ЮТТесты
.ДобавитьТестовыйНабор("Проведение документа")
.ВТранзакции() // ← изоляция: откат после каждого теста
.ДобавитьСерверныйТест("ТестПроведениеСДоговором")
.ДобавитьСерверныйТест("ТестПроведениеСКорректнойСуммой");
КонецПроцедуры
ЮТест.Данные()Create catalog items through ЮТест.Данные().СоздатьЭлемент(...) or КонструкторОбъекта(...).Записать(). Such objects are tracked by YaxUnit and deleted automatically. A direct call to Справочники.X.СоздатьЭлемент() is an antipattern: the object is not tracked and remains in the database.
СоздатьДокумент() - mandatory teardownЮТест.Данные().СоздатьДокумент(...) is tracked and deleted automatically. But if a document is created directly through Документы.X.СоздатьДокумент(), it is NOT tracked, and an explicit teardown in .После("ИмяПроцедурыОчистки") is required.
.ВТранзакции()There are three situations where .ВТранзакции() MUST NOT be used - in each case, a justification comment is required on the set and teardown through .После():
| Situation | Reason for exclusion | Isolation method |
|---|---|---|
| (a) Negative posting test (expected Отказ) | A failed nested transaction poisons the outer one: "Errors have already occurred in this transaction!" on subsequent reads | .УдалениеТестовыхДанных() + .После("Очистка") |
| (b) Prod code with ТранзакцияАктивна() guard | Two-phase commits, real API calls, registers with a unique key - fail or behave unpredictably inside a transaction | .После("Очистка") with manual cleanup |
| (c) Client context | ДобавитьКлиентскийТест - transactional rollback on the client is unavailable due to platform architecture | Перед/После handlers with server context |
// Exception (a): negative test - the expected Отказ poisons the outer transaction.
// Isolation: ЮТест.Данные() + .УдалениеТестовыхДанных() + teardown in .После().
ЮТТесты
.ДобавитьТестовыйНабор("Запрет проведения")
.УдалениеТестовыхДанных()
.После("ОчиститьДокументыЗапретПроведения")
.ДобавитьСерверныйТест("ТестЗапретБезДоговора");
A test that changes a document's write mode must reread the object between mode changes - this models form behavior:
// Провести
ДокОбъект = ДокСсылка.ПолучитьОбъект();
ДокОбъект.Записать(РежимЗаписиДокумента.Проведение);
// Отменить проведение — перечитываем, как форма
ДокОбъект = ДокСсылка.ПолучитьОбъект();
ДокОбъект.Записать(РежимЗаписиДокумента.ОтменаПроведения);
Platform 8.3.27 limitation: programmatic reposting in a server session sometimes produces [ОшибкаХранимыхДанных] - a stack without application frames, and neither rereading nor .ВТранзакции() helps. In that case, reposting idempotency is verified at the scenario layer (Vanessa), and the unit test is recorded via ЮТест.Пропустить() with an explicit justification.
Isolation is declared in code (.ВТранзакции(), teardown), but it is proven by facts - counters in the DB before/after the run. A green run does NOT prove cleanliness: a test may pass and still leave garbage behind.
Перед handlers and test bodies (catalogs, documents, registers), including context helper objects (СоздатьКонтекстТеста and so on).ВЫБРАТЬ КОЛИЧЕСТВО(*) ИЗ Справочник.X (platform query: MCP execute_query / query console). For information registers, count by the test marker dimension, not the whole table.ВЫБРАТЬ Наименование ... ГДЕ Наименование ПОДОБНО "...%"), identify the creating handler/test, add teardown, repeat the checklist from step 2.Pitfalls (precedent TASK-173 / TASK-165.7):
| Pitfall | Essence |
|---|---|
| YaxUnit auto-delete fails on .Записать(Ложь, Истина) | УстановитьПометкуУдаления revalidates required fields and refuses - the object remains. Teardown is physical deletion: Объект.ОбменДанными.Загрузка = Истина; Объект.Удалить(); |
| Partial teardown | The После handler cleans only part of what was created (for example, the semaphore register but not the account catalog) - it looks like teardown, but still leaves garbage |
| "GREEN = clean" | 41/41 tests are green, while one module still left +15 objects per run - detected only by counters |
| Antipattern | Correct |
|-------------|-----------|
| Data in ИсполняемыеСценарии | Data in the test body or Перед handler |
| One test checks 10 conditions | One test - one assertion |
| Test depends on execution order | Each test is isolated |
| Hardcoded references to infobase objects | Create through ЮТест.Данные() |
| Testing private logic | Test through the public interface |
| Mocking the module under test | Mock only dependencies |
| Writable set without .ВТранзакции() | .ВТранзакции() by default; exceptions - with a comment + teardown |
| Справочники.X.СоздатьЭлемент() in a test | ЮТест.Данные().СоздатьЭлемент() - tracked and deleted automatically |
| .ВТранзакции() on a negative posting test | Exception (a) - poisons the transaction; use .УдалениеТестовыхДанных() + .После() |
| "GREEN = clean" acceptance without counters | Self-cleanup checklist: counters before/after by queries, two runs, delta 0 |
Tests and implementation are written by different agents in different phases. The test author does not know the implementation, and the code author does not modify tests.
Phase 3a: Scenario-Author → .feature (BDD) ┐ parallel
Phase 3b: Developer-Tests → unit tests (Red) ┘
Phase 3c: Developer-Code → code (Green)
Phase 4: Tester → edge cases, regression, BDD + unit
| Layer | Phase | Agent | Covers | |------|------|-------|-----------| | BDD (acceptance) | 3a | Scenario-Author | Behavior through the UI | | TDD (unit) | 3b | Developer-Tests | Public methods, MUST scenarios, basic negatives | | TDD (green) | 3c | Developer-Code | Implementation that passes unit tests | | Coverage | 4 | Tester | Edge cases, integration, regression |
Phase 3a and 3b run in parallel. Phase 3c starts after both are complete.
Тест упал
├── Ошибка в тесте → Tester исправляет (test_error)
└── Баг в коде → СТОП. Метка implementation_error + описание.
Оркестратор возвращает задачу Developer.
User/Role context in Test Plan: if the code uses SetPrivilegedMode, role checks (AccessRight, RoleAvailable), or the result depends on the current user, the specification MUST explicitly state for each test in the "Test Plan" section: user name / role set, required mode (privileged or not), expected result (success / refusal). Without this, a test under a full-rights runner (for example AgentAI) will produce a false positive: it will pass "by coincidence" through the privileged branch without checking role-dependent behavior. If this is technically impossible for unit tests, it is recorded in the spec as a separate ADR with a move to 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.