Модульное тестирование навыков
После написания навыка как убедиться, что он действительно работает так, как ожидается? Ручное тестирование отнимает много времени и подвержено ошибкам.
В этой статье рассказывается, как создать систематические тест-кейсы для навыка — от модульного тестирования скриптов до сквозного тестирования триггеров.
Два уровня тестирования навыков
| Уровень | Объект тестирования | Метод |
|---|---|---|
| Модульное тестирование скриптов | Скрипты Python/JS в scripts/ | pytest / стандартный unittest |
| Сквозное тестирование на основе триггеров | Правильно ли срабатывает навык и соответствует ли вывод ожиданиям | фреймворк оценки skill-creator |
Уровень 1: Модульное тестирование скриптов
Скрипты — самая детерминированная часть навыка, и они хорошо подходят для покрытия с помощью стандартного фреймворка модульного тестирования.
Пример
import pytest
import os
import tempfile
import csv
# Тестируемый модуль
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from clean_data import remove_duplicates, fill_nulls, strip_whitespace
# ── Тест: Дедупликация ──────────────────────────────────────────
def test_remove_duplicates_basic():
"""Обычный случай: дублирующиеся строки должны быть удалены"""
rows = [
{"name": "example", "score": "90"},
{"name": "example", "score": "90"}, # Дублирующая строка
{"name": "EXAMPLE", "score": "80"},
]
result = remove_duplicates(rows)
assert len(result) == 2, "1 дублирующая строка должна быть удалена"
def test_remove_duplicates_empty():
"""Граничный случай: пустой список не должен вызывать ошибку"""
result = remove_duplicates([])
assert result == []
# ── Тест: заполнение пустых значений ──────────────────────────────────────
def test_fill_nulls_numeric():
"""Пустые значения в числовых столбцах должны заполняться 0"""
rows = [{"value": "10"}, {"value": ""}, {"value": "20"}]
result = fill_nulls(rows, col="value", fill_with="0")
assert result[1]["value"] == "0"
# ── Тест: удаление пробелов ──────────────────────────────────────
def test_strip_whitespace():
"""Начальные и конечные пробелы в строках должны удаляться"""
rows = [{"name": " example "}, {"name": "EXAMPLE"}]
result = strip_whitespace(rows, col="name")
assert result[0]["name"] == "example"
assert result[1]["name"] == "EXAMPLE"
# ── Интеграционный тест: чтение реального файла ──────────────────────────────
def test_process_real_file():
"""Создайте временный CSV-файл и протестируйте полный процесс обработки"""
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv",
delete=False, newline="") as f:
writer = csv.DictWriter(f, fieldnames=["name", "score"])
writer.writeheader()
writer.writerow({"name": " example ", "score": "90"})
writer.writerow({"name": " example ", "score": "90"}) # Дубликат
writer.writerow({"name": "EXAMPLE", "score": ""}) # Нулевое значение
tmp_path = f.name
try:
from clean_data import process_file
result = process_file(tmp_path)
assert result["removed_rows"] == 1
assert result["fixed_values"] == 1
finally:
os.unlink(tmp_path)
Запустите тесты:
# 进入 Skill 目录后运行所有测试 cd my-skill/ pytest scripts/tests/ -v # 生成覆盖率报告 pytest scripts/tests/ --cov=scripts --cov-report=term-missing
Вывод:
scripts/tests/test_clean_data.py::test_remove_duplicates_basic PASSED scripts/tests/test_clean_data.py::test_remove_duplicates_empty PASSED scripts/tests/test_clean_data.py::test_fill_nulls_numeric PASSED scripts/tests/test_clean_data.py::test_strip_whitespace PASSED scripts/tests/test_clean_data.py::test_process_real_file PASSED 5 passed in 0.12s
Уровень 2: Сквозное тестирование на основе триггеров
Тестирование триггеров проверяет, будет ли Claude использовать Skill, когда пользователь отправляет запрос.
Тестовые случаи определяются в формате JSON и содержат пользовательские запросы и проверки ожидаемого поведения.
Пример
{
"id": "trigger_basic",
"prompt": "Помогите мне проанализировать эти данные о продажах в CSV, найти тенденции по месяцам и сгенерировать статистическую сводку",
"assertions": [
{
"type": "skill_triggered",
"skill": "csv-analyzer",
"description": "Сложные запросы на анализ должны активировать csv-analyzer"
}
]
},
{
"id": "trigger_with_file",
"prompt": "Я загрузил example_sales.csv, пожалуйста, проанализируйте распределение данных по каждому столбцу",
"assertions": [
{
"type": "skill_triggered",
"skill": "csv-analyzer"
},
{
"type": "output_contains",
"keyword": "Statistics",
"description": "Вывод должен содержать статистическую информацию"
}
]
},
{
"id": "no_trigger_simple",
"prompt": "В каком формате CSV?",
"assertions": [
{
"type": "skill_not_triggered",
"skill": "csv-analyzer",
"description": "Простые вопросы на знание не должны активировать Skill"
}
]
}
]
Запустите тесты срабатывания (требуется инструмент eval, предоставляемый skill-creator):
Пример
python -m scripts.run_eval \
--eval-set evals/trigger-eval.json \
--skill-path csv-analyzer/ \
--model claude-sonnet-4-20250514
# Сгенерируйте HTML-отчёт для ручной проверки
python eval-viewer/generate_review.py \
--results evals/results/ \
--output evals/review.html
Промпты тест-кейсов должны быть достаточно сложными. Для простых запросов вроде "read CSV", даже если срабатывание не происходит, это не значит, что у Skill есть проблема, потому что Claude может справиться с этим самостоятельно. Хорошие тест-кейсы должны быть многошаговыми запросами, которые пользователи действительно могли бы сделать, с чёткими требованиями к выводу.
Покрытие тест-кейсов
| Type | Сценарии для покрытия |
|---|---|
| Положительное срабатывание | Сложные задачи, содержащие ключевые слова, чёткие требования к формату вывода |
| Отрицательное срабатывание | Простые вопросы и ответы, только упоминающие соответствующие термины, но не требующие Навыка |
| Граничные входные данные | Пустые файлы, файлы слишком большого размера, файлы с неподдерживаемыми форматами |
| Восстановление после ошибок | Понятно ли сообщение об ошибке, когда путь к файлу указан неверно |