Написание тестов — одна из задач, где OpenClaw экономит больше всего времени. Агент читает реализацию, понимает логику и генерирует полное тестовое покрытие за минуты. Этот гайд — о том, как встроить это в ежедневный workflow.
Базовый промпт для генерации тестов
Самый простой способ — открыть OpenClaw в папке проекта и попросить:
> Напиши unit-тесты для функции validateEmail в src/utils/validation.ts.
Используй Vitest. Покрой: валидные email'ы, невалидные, граничные случаи (пустая строка, null, очень длинный).
Файл тестов: src/utils/validation.test.ts
OpenClaw прочитает файл, поймёт сигнатуру функции и напишет тесты.
Пример: тесты для TypeScript-функции
Исходный код src/utils/pricing.ts:
export function calculateDiscount(
price: number,
discountPercent: number,
userTier: "basic" | "premium" | "enterprise"
): number {
if (price < 0) throw new Error("Price cannot be negative");
if (discountPercent < 0 || discountPercent > 100) {
throw new Error("Discount must be between 0 and 100");
}
const tierMultiplier = { basic: 1, premium: 1.1, enterprise: 1.25 };
const effectiveDiscount = discountPercent * tierMultiplier[userTier];
return price * (1 - Math.min(effectiveDiscount, 100) / 100);
}
Промпт OpenClaw:
> Напиши исчерпывающие Vitest-тесты для calculateDiscount.
Файл: src/utils/pricing.test.ts
Покрой все ветки: разные tier'ы, граничные значения, исключения.
Используй describe/it, добавь комментарии для каждой группы тестов.
Сгенерированные тесты:
import { describe, it, expect } from "vitest";
import { calculateDiscount } from "./pricing";
describe("calculateDiscount", () => {
describe("basic tier", () => {
it("applies discount correctly", () => {
expect(calculateDiscount(100, 10, "basic")).toBe(90);
});
it("returns full price when discount is 0", () => {
expect(calculateDiscount(100, 0, "basic")).toBe(100);
});
it("returns 0 for 100% discount", () => {
expect(calculateDiscount(100, 100, "basic")).toBe(0);
});
});
describe("premium tier", () => {
it("applies 10% tier multiplier to discount", () => {
// 20% скидка × 1.1 = 22%, цена = 78
expect(calculateDiscount(100, 20, "premium")).toBeCloseTo(78, 5);
});
it("caps effective discount at 100%", () => {
// 100% × 1.1 = 110% → ограничивается до 100%
expect(calculateDiscount(100, 100, "premium")).toBe(0);
});
});
describe("enterprise tier", () => {
it("applies 25% tier multiplier to discount", () => {
// 20% скидка × 1.25 = 25%, цена = 75
expect(calculateDiscount(100, 20, "enterprise")).toBe(75);
});
});
describe("validation errors", () => {
it("throws for negative price", () => {
expect(() => calculateDiscount(-1, 10, "basic")).toThrow("Price cannot be negative");
});
it("throws for discount below 0", () => {
expect(() => calculateDiscount(100, -1, "basic")).toThrow("Discount must be between 0 and 100");
});
it("throws for discount above 100", () => {
expect(() => calculateDiscount(100, 101, "basic")).toThrow("Discount must be between 0 and 100");
});
});
describe("edge cases", () => {
it("handles zero price", () => {
expect(calculateDiscount(0, 50, "basic")).toBe(0);
});
it("handles fractional prices", () => {
expect(calculateDiscount(9.99, 10, "basic")).toBeCloseTo(8.991, 3);
});
});
});
Тесты для Python с pytest
> Напиши pytest-тесты для класса UserRepository в app/repositories/user.py.
Используй pytest-mock для мокирования базы данных.
Покрой: create, get_by_id, update, delete, not found кейс.
# tests/test_user_repository.py
import pytest
from unittest.mock import MagicMock, patch
from app.repositories.user import UserRepository
from app.models.user import User
@pytest.fixture
def mock_db():
return MagicMock()
@pytest.fixture
def repo(mock_db):
return UserRepository(mock_db)
class TestUserRepository:
def test_create_user(self, repo, mock_db):
user_data = {"name": "Alice", "email": "alice@example.com"}
mock_db.execute.return_value.lastrowid = 42
result = repo.create(user_data)
mock_db.execute.assert_called_once()
assert result.id == 42
def test_get_by_id_found(self, repo, mock_db):
mock_db.execute.return_value.fetchone.return_value = {
"id": 1, "name": "Bob", "email": "bob@example.com"
}
user = repo.get_by_id(1)
assert user is not None
assert user.name == "Bob"
def test_get_by_id_not_found(self, repo, mock_db):
mock_db.execute.return_value.fetchone.return_value = None
user = repo.get_by_id(999)
assert user is None
def test_update_user(self, repo, mock_db):
mock_db.execute.return_value.rowcount = 1
result = repo.update(1, {"name": "Bob Updated"})
assert result is True
def test_delete_user(self, repo, mock_db):
mock_db.execute.return_value.rowcount = 1
result = repo.delete(1)
assert result is True
Автогенерация тестов при коммите (Git Hook)
Настройте pre-commit хук, который генерирует тесты для изменённых файлов:
# .git/hooks/pre-commit
#!/bin/bash
# Находим изменённые .ts файлы без тестов
CHANGED=$(git diff --cached --name-only | grep '\.ts$' | grep -v '\.test\.')
for file in $CHANGED; do
test_file="${file%.ts}.test.ts"
if [ ! -f "$test_file" ]; then
echo "⚠️ No tests for: $file"
echo "Generating tests with OpenClaw..."
openclaw --no-interactive << EOF
Напиши Vitest unit-тесты для $file.
Создай файл $test_file.
Используй describe/it, покрой основные кейсы и граничные значения.
EOF
git add "$test_file"
echo "✓ Tests generated: $test_file"
fi
done
chmod +x .git/hooks/pre-commit
Интеграция в CI/CD с GitHub Actions
# .github/workflows/test-coverage.yml
name: Test Coverage
on: [pull_request]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci
- name: Run tests with coverage
run: npm run test -- --coverage
- name: Check coverage threshold
run: |
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
echo "Coverage: $COVERAGE%"
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "❌ Coverage below 80%"
exit 1
fi
- name: Comment coverage on PR
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const summary = JSON.parse(fs.readFileSync('coverage/coverage-summary.json'));
const pct = summary.total.lines.pct;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Test Coverage: ${pct}% lines\n${pct >= 80 ? '✅' : '❌'} Threshold: 80%`
});
Продвинутый промпт: TDD-режим
Если хотите писать тесты до реализации:
> Напиши тесты для функции formatPhoneNumber которую я сейчас буду реализовывать.
Требования:
- Принимает строку с номером телефона в любом формате
- Возвращает формат: +7 (999) 123-45-67
- Бросает ошибку если номер невалиден
- Поддерживает: +7, 8, 7 в начале
Файл: src/utils/phone.test.ts
Реализации ещё нет — пиши только тесты.
OpenClaw напишет тесты-спецификации. Вы реализуете функцию под них.
Тестирование React-компонентов
> Напиши тесты для компонента LoginForm в src/components/LoginForm.tsx.
Используй Vitest + React Testing Library.
Покрой: рендеринг формы, валидацию полей, успешный сабмит, отображение ошибки от API.
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./LoginForm";
describe("LoginForm", () => {
it("renders email and password fields", () => {
render(<LoginForm onSubmit={vi.fn()} />);
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
});
it("shows validation error for invalid email", async () => {
render(<LoginForm onSubmit={vi.fn()} />);
await userEvent.type(screen.getByLabelText(/email/i), "not-an-email");
await userEvent.click(screen.getByRole("button", { name: /войти/i }));
expect(screen.getByText(/неверный формат email/i)).toBeInTheDocument();
});
it("calls onSubmit with credentials on valid form", async () => {
const onSubmit = vi.fn().mockResolvedValue({ success: true });
render(<LoginForm onSubmit={onSubmit} />);
await userEvent.type(screen.getByLabelText(/email/i), "user@example.com");
await userEvent.type(screen.getByLabelText(/password/i), "password123");
await userEvent.click(screen.getByRole("button", { name: /войти/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: "user@example.com",
password: "password123",
});
});
});
});
Советы по качеству
Проверяйте тесты перед коммитом: npm run test — и только потом в Git.
Просите конкретику: «покрой граничные значения и исключения» лучше чем «напиши тесты».
Итерируйте: если тест не проходит, покажите агенту ошибку — он исправит.
Не копируйте слепо: просматривайте сгенерированные тесты. Иногда агент предполагает поведение, которое нужно уточнить.
Итог
OpenClaw генерирует тесты быстрее, чем их пишет человек — но важно понимать что они покрывают. Используйте агента для рутинного покрытия, сами пишите тесты для критической бизнес-логики. Такая комбинация даёт и скорость, и надёжность.
Читайте также: VS Code workflow разработчика и GitHub интеграция.