Модульное тестирование навыков

После написания навыка как убедиться, что он действительно работает так, как ожидается? Ручное тестирование отнимает много времени и подвержено ошибкам.

В этой статье рассказывается, как создать систематические тест-кейсы для навыка — от модульного тестирования скриптов до сквозного тестирования триггеров.


Два уровня тестирования навыков

УровеньОбъект тестированияМетод
Модульное тестирование скриптовСкрипты Python/JS в scripts/pytest / стандартный unittest
Сквозное тестирование на основе триггеровПравильно ли срабатывает навык и соответствует ли вывод ожиданиямфреймворк оценки skill-creator

Уровень 1: Модульное тестирование скриптов

Скрипты — самая детерминированная часть навыка, и они хорошо подходят для покрытия с помощью стандартного фреймворка модульного тестирования.

Пример

# Путь к файлу: scripts/tests/test_clean_data.py
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Сценарии для покрытия
Положительное срабатываниеСложные задачи, содержащие ключевые слова, чёткие требования к формату вывода
Отрицательное срабатываниеПростые вопросы и ответы, только упоминающие соответствующие термины, но не требующие Навыка
Граничные входные данныеПустые файлы, файлы слишком большого размера, файлы с неподдерживаемыми форматами
Восстановление после ошибокПонятно ли сообщение об ошибке, когда путь к файлу указан неверно
Другие расширения