Тестирование с AI — как ИИ пишет тесты за тебя

Зачем тесты и как AI меняет подход к TDD
Заголовок раздела «Зачем тесты и как AI меняет подход к TDD»Раньше тесты писали последними — или не писали вообще. Было лениво, долго, скучно. Теперь AI пишет тесты за тебя — быстрее, полнее и с edge cases, о которых ты бы не подумал.
Классический TDD (Test-Driven Development) говорит: сначала тест, потом код. С AI это реально работает:
- Описываешь поведение функции на русском
- AI генерирует тесты
- AI пишет код, чтобы тесты проходили
- Ты ревьюишь, а не пишешь
Результат: тест-покрытие 80%+ без боли. Меньше багов в проде. Коллеги уважают.
Unit-тесты с Jest и Vitest
Заголовок раздела «Unit-тесты с Jest и Vitest»Генерация тестов из кода
Заголовок раздела «Генерация тестов из кода»Открой функцию в Cursor/Claude Code и напиши:
Напиши unit-тесты для этой функции. Покрой:- happy path- edge cases (null, undefined, пустые массивы)- граничные значения- ошибкиИспользуй Vitest + TypeScript.Пример — функция для форматирования цены:
export function formatPrice(amount: number, currency = 'EUR'): string { if (amount < 0) throw new Error('Price cannot be negative') return new Intl.NumberFormat('ru-RU', { style: 'currency', currency, }).format(amount)}AI генерирует тесты:
import { describe, it, expect } from 'vitest'import { formatPrice } from './formatPrice'
describe('formatPrice', () => { it('форматирует целое число в евро', () => { expect(formatPrice(100)).toContain('100') expect(formatPrice(100)).toContain('€') })
it('форматирует дробное число', () => { const result = formatPrice(19.99) expect(result).toContain('19') expect(result).toContain('99') })
it('работает с разными валютами', () => { const usd = formatPrice(50, 'USD') expect(usd).toContain('$') })
it('выбрасывает ошибку при отрицательной цене', () => { expect(() => formatPrice(-1)).toThrow('Price cannot be negative') })
it('обрабатывает ноль', () => { expect(formatPrice(0)).toContain('0') })
it('обрабатывает очень большие числа', () => { expect(() => formatPrice(999_999_999)).not.toThrow() })})Видишь? 6 тест-кейсов, включая граничные значения. Руками написал бы 2-3, и то не сразу.
Промпты для разных сценариев
Заголовок раздела «Промпты для разных сценариев»У меня есть такая логика (описание словами).Напиши: 1) TypeScript функцию, 2) тесты Vitest к ней.Начни с тестов (TDD-стиль).Вот функция [вставь код].Напиши исчерпывающие тесты Vitest.Особое внимание: null/undefined, пустые коллекции,граничные значения, async операции.Вот функция и существующие тесты.Coverage report показывает непокрытые строки: [вставь].Добавь тесты, чтобы покрыть их.Настройка Vitest за 2 минуты
Заголовок раздела «Настройка Vitest за 2 минуты»npm install -D vitest @vitest/uiimport { defineConfig } from 'vitest/config'
export default defineConfig({ test: { environment: 'node', coverage: { reporter: ['text', 'html'], threshold: { lines: 80 } } }}){ "scripts": { "test": "vitest", "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage" }}E2E тесты с Playwright через AI
Заголовок раздела «E2E тесты с Playwright через AI»End-to-end тесты — самые сложные для написания и самые ценные. AI резко снижает порог входа.
Генерация E2E из описания сценария
Заголовок раздела «Генерация E2E из описания сценария»Напиши Playwright E2E тест для такого сценария:- Пользователь открывает /login- Вводит email: [email protected], пароль: Password123- Кликает "Войти"- Должен увидеть дашборд с текстом "Добро пожаловать"- Проверь что URL стал /dashboard
TypeScript, используй Page Object Model.AI пишет:
import { test, expect } from '@playwright/test'import { LoginPage } from './pages/LoginPage'
test.describe('Аутентификация', () => { test('успешный логин перенаправляет на дашборд', async ({ page }) => { const loginPage = new LoginPage(page)
await loginPage.goto()
await expect(page).toHaveURL('/dashboard') await expect(page.getByText('Добро пожаловать')).toBeVisible() })
test('неверный пароль показывает ошибку', async ({ page }) => { const loginPage = new LoginPage(page)
await loginPage.goto()
await expect(page.getByRole('alert')).toContainText('Неверный пароль') await expect(page).toHaveURL('/login') })})import { Page } from '@playwright/test'
export class LoginPage { constructor(private page: Page) {}
async goto() { await this.page.goto('/login') }
async login(email: string, password: string) { await this.page.getByLabel('Email').fill(email) await this.page.getByLabel('Пароль').fill(password) await this.page.getByRole('button', { name: 'Войти' }).click() }}Playwright Codegen + AI
Заголовок раздела «Playwright Codegen + AI»Playwright умеет записывать действия браузера и генерировать тесты. AI доводит их до ума:
npx playwright codegen http://localhost:3000Получил сырой тест? Отдай в AI:
Вот записанный Playwright тест (сырой).Рефактори:- Вынеси в Page Object Model- Добавь осмысленные assertions- Убери хрупкие селекторы, используй роли и лейблы- Добавь тест для негативного сценарияКак просить AI покрыть edge cases
Заголовок раздела «Как просить AI покрыть edge cases»Это суперсила. AI видит edge cases, которые ты пропустишь.
Промпт для edge case охоты
Заголовок раздела «Промпт для edge case охоты»Вот функция [код].Найди все возможные edge cases и для каждого:1. Опиши проблему2. Напиши тест который её поймает3. Предложи фикс если нужен
Думай как опытный QA инженер.Типичные edge cases которые AI находит
Заголовок раздела «Типичные edge cases которые AI находит»| Категория | Примеры |
|---|---|
| Null/undefined | null, undefined, не передан аргумент |
| Пустые данные | [], {}, "", 0 |
| Граничные числа | Infinity, NaN, Number.MAX_SAFE_INTEGER |
| Строки | Пустая строка, Unicode, эмодзи, HTML-инъекции |
| Асинхронность | Timeout, параллельные вызовы, race conditions |
| Типы | Число вместо строки, массив вместо объекта |
Пример — функция парсинга JSON с AI-найденными edge cases:
describe('safeParseJson', () => { it('парсит валидный JSON', () => { /* ... */ }) it('возвращает null при невалидном JSON', () => { /* ... */ }) it('обрабатывает пустую строку', () => { /* ... */ }) it('обрабатывает null на входе', () => { /* ... */ }) it('парсит вложенные объекты', () => { /* ... */ }) it('обрабатывает числа и булевы значения', () => { /* ... */ }) it('не падает на очень длинных строках', () => { /* ... */ }) // ← последние 3 нашёл AI, не я})AI Code Review с фокусом на баги
Заголовок раздела «AI Code Review с фокусом на баги»Прежде чем пушить — прогони через AI-ревью. Это работает лучше статических анализаторов.
Промпт для code review
Заголовок раздела «Промпт для code review»Сделай code review этого кода с фокусом на:1. Потенциальные баги (NPE, off-by-one, race conditions)2. Уязвимости безопасности3. Проблемы производительности4. Места без error handling5. Сложную логику которую стоит протестировать
Для каждой проблемы: покажи строку, объясни риск, предложи фикс.Пример — что AI находит в “простом” коде
Заголовок раздела «Пример — что AI находит в “простом” коде»// ❌ Проблемный кодasync function getUserData(userId: string) { const user = await db.users.findOne({ id: userId }) const posts = await db.posts.find({ authorId: user.id }) // NPE если user = null! return { user, posts: posts.slice(0, 10) }}AI скажет:
- Строка 3:
userможет бытьnull→user.idупадёт с TypeError - Строка 4:
postsможет бытьnull→.slice()упадёт - Производительность: два последовательных запроса, лучше параллельно через
Promise.all - Нет логирования ошибок
// ✅ После AI code reviewasync function getUserData(userId: string) { const user = await db.users.findOne({ id: userId }) if (!user) return null
const posts = await db.posts.find({ authorId: user.id }) ?? [] return { user, posts: posts.slice(0, 10) }}Инструменты: что использовать
Заголовок раздела «Инструменты: что использовать»CodiumAI / Qodo
Заголовок раздела «CodiumAI / Qodo»Плагин для VS Code/JetBrains. Специализируется именно на тестах:
- Анализирует функцию и предлагает тест-кейсы в сайдбаре
- Показывает какие edge cases не покрыты
- Интегрируется с Jest, Vitest, Pytest, JUnit
# Установка в VS Code# Extensions → искать "Qodo Gen" (бывший CodiumAI)GitHub Copilot для тестов
Заголовок раздела «GitHub Copilot для тестов»Copilot умеет генерировать тесты через /tests в чате:
# В Copilot Chat:/tests # Генерирует тесты для выделенного кода
# Или с контекстом:Write comprehensive tests for #file:utils/auth.tsFocus on security edge casesCursor — агентный режим для тестов
Заголовок раздела «Cursor — агентный режим для тестов»В Cursor можно поставить агенту задачу целиком:
Добавь тест-покрытие для всего файла src/utils/pricing.ts.Используй Vitest. Запусти тесты и убедись что все проходят.Покрытие должно быть не менее 90%.Агент сам пишет тесты, запускает npm test, смотрит что упало, фиксит — и так по кругу пока не добьётся 90%.
Сравнение инструментов
Заголовок раздела «Сравнение инструментов»| Инструмент | Сила | Когда использовать |
|---|---|---|
| CodiumAI/Qodo | Специализация на тестах | Хочешь именно тест-генерацию |
Copilot /tests | Быстро, inline | Уже работаешь в VS Code с Copilot |
| Cursor Agent | Итеративно, сам запускает | Нужно высокое покрытие с нуля |
| Claude/ChatGPT chat | Гибко, объяснения | Сложная логика, нужен анализ |
Практика: пишем тесты для React компонента с AI
Заголовок раздела «Практика: пишем тесты для React компонента с AI»Возьмём реальный сценарий — компонент SearchBar с дебаунсом.
Шаг 1 — компонент
Заголовок раздела «Шаг 1 — компонент»import { useState, useEffect } from 'react'
interface SearchBarProps { onSearch: (query: string) => void debounceMs?: number placeholder?: string}
export function SearchBar({ onSearch, debounceMs = 300, placeholder = 'Поиск...'}: SearchBarProps) { const [value, setValue] = useState('')
useEffect(() => { const timer = setTimeout(() => { if (value.trim()) onSearch(value.trim()) }, debounceMs)
return () => clearTimeout(timer) }, [value, debounceMs, onSearch])
return ( <input type="search" value={value} onChange={e => setValue(e.target.value)} placeholder={placeholder} aria-label="Поисковая строка" /> )}Шаг 2 — просим AI написать тесты
Заголовок раздела «Шаг 2 — просим AI написать тесты»Вот React компонент SearchBar с дебаунсом.Напиши тесты с Vitest + React Testing Library + @testing-library/user-event.
Покрой:- Рендер с дефолтными пропсами- Рендер с кастомным placeholder- onSearch вызывается после debounce- onSearch НЕ вызывается сразу при вводе- onSearch НЕ вызывается для пустой строки/пробелов- Быстрый ввод нескольких символов — только один вызов- Кастомный debounceMs работает корректно
Используй vi.useFakeTimers() для контроля времени.Шаг 3 — результат от AI
Заголовок раздела «Шаг 3 — результат от AI»import { render, screen } from '@testing-library/react'import userEvent from '@testing-library/user-event'import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'import { SearchBar } from './SearchBar'
describe('SearchBar', () => { beforeEach(() => vi.useFakeTimers()) afterEach(() => vi.restoreAllMocks())
it('рендерится с дефолтным placeholder', () => { render(<SearchBar onSearch={vi.fn()} />) expect(screen.getByPlaceholderText('Поиск...')).toBeInTheDocument() })
it('рендерится с кастомным placeholder', () => { render(<SearchBar onSearch={vi.fn()} placeholder="Найти урок..." />) expect(screen.getByPlaceholderText('Найти урок...')).toBeInTheDocument() })
it('вызывает onSearch после debounce', async () => { const onSearch = vi.fn() const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
render(<SearchBar onSearch={onSearch} />) await user.type(screen.getByRole('searchbox'), 'react')
expect(onSearch).not.toHaveBeenCalled() vi.advanceTimersByTime(300) expect(onSearch).toHaveBeenCalledWith('react') expect(onSearch).toHaveBeenCalledTimes(1) })
it('НЕ вызывает onSearch для пустой строки', async () => { const onSearch = vi.fn() const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
render(<SearchBar onSearch={onSearch} />) await user.type(screen.getByRole('searchbox'), ' ')
vi.advanceTimersByTime(300) expect(onSearch).not.toHaveBeenCalled() })
it('быстрый ввод — только один вызов onSearch', async () => { const onSearch = vi.fn() const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
render(<SearchBar onSearch={onSearch} />) const input = screen.getByRole('searchbox')
await user.type(input, 'r') vi.advanceTimersByTime(100) await user.type(input, 'e') vi.advanceTimersByTime(100) await user.type(input, 'act') vi.advanceTimersByTime(300)
expect(onSearch).toHaveBeenCalledTimes(1) expect(onSearch).toHaveBeenCalledWith('react') })
it('кастомный debounceMs работает корректно', async () => { const onSearch = vi.fn() const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
render(<SearchBar onSearch={onSearch} debounceMs={500} />) await user.type(screen.getByRole('searchbox'), 'test')
vi.advanceTimersByTime(300) expect(onSearch).not.toHaveBeenCalled()
vi.advanceTimersByTime(200) expect(onSearch).toHaveBeenCalledWith('test') })})Запускаем:
npm test -- SearchBar# ✓ рендерится с дефолтным placeholder# ✓ рендерится с кастомным placeholder# ✓ вызывает onSearch после debounce# ✓ НЕ вызывает onSearch для пустой строки# ✓ быстрый ввод — только один вызов onSearch# ✓ кастомный debounceMs работает корректно# Tests: 6 passedИтог: твой AI-тест workflow
Заголовок раздела «Итог: твой AI-тест workflow»1. Пишешь функцию/компонент ↓2. Просишь AI написать тесты (с edge cases!) ↓3. Запускаешь тесты — смотришь что упало ↓4. Просишь AI объяснить падение и предложить фикс ↓5. Перед PR — просишь AI сделать code review ↓6. Пушишь уверенноС AI тесты перестают быть болью. Они становятся быстрее, полнее и интереснее. Попробуй один раз — потом не захочешь без них.