Написание тестов — рутина, в которой почти нет творчества, но очень много проектных договорённостей. Какой раннер, где лежат файлы, как они называются, что мокать, а что дёргать по-настоящему, какое покрытие считается достаточным — всё это в каждой команде своё. Каждый раз пересказывать эти правила агенту заново означает тратить время и получать разный результат у разных разработчиков.
В этом уроке мы соберём скилл /write-tests <путь-к-файлу> и доведём его от наивной первой версии до рабочей. Попутно разберём, почему одни решения работают, а другие ломаются, и как выглядит проверка того, что скилл действительно полезен.
Что уже умеем и что добавим
Вы уже умеете создавать скилл, писать description, подставлять аргументы через $ARGUMENTS, индексные $0 и $1 и именованные плейсхолдеры из arguments. Теперь применим всё это к одной сквозной задаче и добавим два новых приёма: динамический контекст через !команда`` и вынос длинных материалов во вспомогательные файлы рядом с SKILL.md.
Что вы узнаете в рамках статьи
- Почему генерация тестов — почти идеальный кандидат для скилла
- Как записать конвенции проекта в виде постоянно действующих инструкций
- Как подтянуть в скилл реальную конфигурацию проекта и пример существующего теста
- Как принять один и два аргумента и что делать, если аргумент не передали
- Когда выносить материалы в
conventions.mdи стоит ли разрешать автовызов
Ключевые термины
- Конвенции тестирования — набор договорённостей проекта: раннер, расположение и именование файлов, подход к мокам, критерий достаточности.
- Динамический контекст — вывод shell-команды, подставленный в тело скилла до отправки модели через синтаксис
!команда``. - Вспомогательный файл —
conventions.mdилиexamples.mdрядом сSKILL.md, на который есть markdown-ссылка; читается только по необходимости. - Индексный плейсхолдер —
$0,$1и далее, короткая форма$ARGUMENTS[N]; отсчёт начинается с нуля.
Постановка и первая версия
Начнём с самого простого, что приходит в голову.
---
description: Пишет тесты для указанного файла.
argument-hint: [путь-к-файлу]
---
Напиши тесты для файла $ARGUMENTS.
Вызов /write-tests src/utils/date.ts сработает: тесты появятся. Проблема в том, какие. Claude ничего не знает о вашем проекте и вынужден угадывать: возьмёт популярный раннер, придумает собственный стиль оформления, положит файл туда, где ему кажется логичным. У одного разработчика получится date.test.ts рядом с исходником, у другого — __tests__/date.spec.ts. Скилл здесь не сэкономил ничего, кроме одной фразы.
Конвенции как постоянно действующие правила
Лечится это переносом договорённостей в тело скилла. Важная деталь механики: отрендеренное содержимое SKILL.md попадает в разговор одним сообщением и остаётся там на последующих ходах, а Claude Code не перечитывает файл заново. Поэтому формулируйте не одноразовый сценарий «сделай шаг, потом шаг», а правила, которые остаются верными и через десять сообщений.
---
description: Генерирует тесты для модуля по конвенциям проекта. Используй, когда просят покрыть файл тестами.
argument-hint: [путь-к-файлу]
---
Целевой модуль: $ARGUMENTS
## Конвенции тестирования проекта
Эти правила действуют для всех тестов, которые вы пишете в этой сессии.
- Раннер — Vitest. Никаких импортов из других фреймворков.
- Файл теста лежит рядом с модулем: `<имя>.test.ts`.
- Внешние HTTP-вызовы и работа с файловой системой мокаются, чистые функции — нет.
- Обязательно покрываются: успешный путь, граничные значения, ошибки.
- Названия блоков на английском, `describe` — имя экспорта, `it` — «should ...».
Если путь к модулю не указан, спросите его и ничего не пишите.
Последняя строка важна. Скилл создаёт файлы, и пустой ввод не должен приводить к тому, что Claude сам выберет, что покрывать.
Динамический контекст: привязка к реальности
Записанные правила легко устаревают. Чтобы скилл опирался на текущее состояние репозитория, подставьте в него вывод команд.
---
description: Генерирует тесты для модуля по конвенциям проекта.
argument-hint: [путь-к-файлу]
allowed-tools: Bash(cat package.json), Bash(ls tests/*)
---
## Конфигурация проекта
!`cat package.json || true`
## Пример существующего теста
!`ls src/**/*.test.ts | head -3 || true`
## Задача
Напишите тесты для `$ARGUMENTS`, следуя конвенциям ниже и совпадая по
стилю с уже существующими тестами.
Два момента здесь принципиальны. Первый: внедрённая команда выполняется до отправки содержимого модели и никогда не спрашивает разрешение — если проверка разрешений не вернула «allow», вся инвокация скилла обрывается. Поэтому команды предодобряются в allowed-tools. Второй: неуспешная команда рушит не свой плейсхолдер, а весь вызов — Claude не увидит содержимое вовсе. Отсюда || true везде, где команда может штатно завершиться ненулевым кодом, например когда файлов ещё нет.
Два аргумента и ловушка пустого ввода
Часто хочется различать типы тестов. Объявите именованные аргументы во frontmatter — они сопоставляются позициям по порядку.
---
description: Генерирует тесты для модуля по конвенциям проекта.
argument-hint: [путь-к-файлу] [тип-тестов]
arguments: [target, kind]
---
В теле используйте $target и $kind. Разница с индексными плейсхолдерами существенна: именованный плейсхолдер без переданного аргумента разворачивается в пустую строку, а вот индексный, например $1 при одном переданном аргументе, останется в тексте как есть — Claude буквально увидит символы $1 и будет гадать, что это. Поэтому при любом варианте в скилле нужна явная инструкция на случай отсутствующего значения: «если тип не указан, считайте его unit».
Структура директории и место в репозитории
Свод конвенций разрастается быстро, а тело скилла — это повторяющаяся стоимость в токенах при каждом вызове. Документация рекомендует держать SKILL.md короче 500 строк, а подробности выносить.
.claude/skills/write-tests/
├── SKILL.md
├── conventions.md
└── examples.md
В SKILL.md оставьте короткую выжимку и ссылки:
## Дополнительные материалы
- Полный свод правил — [conventions.md](conventions.md)
- Разобранные примеры тестов — [examples.md](examples.md)
Имя команды берётся из имени директории, поэтому каталог обязан называться write-tests. Кладите скилл в .claude/skills/ и коммитьте в git: конвенции тестирования — командная договорённость, они должны быть одинаковыми у всех.
Остаётся решить, разрешать ли автовызов. Скилл создаёт файлы, и disable-model-invocation: true гарантирует, что он сработает только по вашей команде, заодно убрав описание из контекста. Обратный аргумент тоже сильный: с автовызовом Claude сам предложит тесты после того, как вы допишете модуль. Выбирайте по тому, насколько вы готовы к незапрошенным файлам в рабочей копии.
Частые ошибки
- Писать инструкции как одноразовый сценарий. Содержимое загружается один раз и живёт дальше; формулировки вида «сначала сделай X» перестают работать на следующем ходе. Пишите правила в настоящем времени.
- Забыть
|| trueв динамической команде. Отсутствие файла или пустой результат обрушат весь вызов скилла, и Claude не получит даже текста инструкций. - Полагаться на индексный плейсхолдер без запасного варианта. Непереданный
$1остаётся в тексте буквально и попадает модели как мусор. - Оценивать скилл по одному запуску. Один удачный модуль ничего не доказывает. Прогоняйте на трёх-четырёх разных файлах и обязательно запускайте сгенерированные тесты.
Наиболее частые вопросы
- Почему
cat package.jsonлучше, чем описание раннера словами? — Потому что файл меняется, а текст в скилле — нет. Динамический вывод всегда актуален. - Что будет, если вызвать
/write-testsбез аргумента? —$ARGUMENTSразвернётся в пустую строку. Именно поэтому в теле нужна инструкция на этот случай. - Обязательно ли перезапускать сессию после правки
SKILL.md? — Нет, правки существующего скилла подхватываются на лету. Перезапуск нужен, если каталог скиллов создан после старта сессии. - Файл можно назвать
skill.md? — Нет, толькоSKILL.md, иначе скилл не будет найден. - Как понять, что скилл готов? — Сгенерированные тесты запускаются и проходят, лежат по конвенции и по стилю неотличимы от написанных руками.
Итоги
Вы прошли путь, который повторяется почти для любого полезного скилла: наивная версия, честный разбор её слабых мест, перенос знаний проекта внутрь, привязка к реальному состоянию репозитория через динамические команды и вынос объёмных материалов в соседние файлы. Получившийся /write-tests экономит не пару фраз, а сам процесс согласования — теперь конвенции живут в репозитории, а не в головах.
Дальше скиллы станут тяжелее: свод правил, вывод команд и примеры вместе занимают заметную часть контекста. На следующем уроке разберём context: fork — способ выполнить скилл в изолированном контексте, чтобы основной разговор оставался чистым.