Перейти к содержимому

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

Тестирование с AI

Раньше тесты писали последними — или не писали вообще. Было лениво, долго, скучно. Теперь AI пишет тесты за тебя — быстрее, полнее и с edge cases, о которых ты бы не подумал.

Классический TDD (Test-Driven Development) говорит: сначала тест, потом код. С AI это реально работает:

  1. Описываешь поведение функции на русском
  2. AI генерирует тесты
  3. AI пишет код, чтобы тесты проходили
  4. Ты ревьюишь, а не пишешь

Результат: тест-покрытие 80%+ без боли. Меньше багов в проде. Коллеги уважают.


Открой функцию в Cursor/Claude Code и напиши:

Напиши unit-тесты для этой функции. Покрой:
- happy path
- edge cases (null, undefined, пустые массивы)
- граничные значения
- ошибки
Используй Vitest + TypeScript.

Пример — функция для форматирования цены:

utils/formatPrice.ts
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 генерирует тесты:

utils/formatPrice.test.ts
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-стиль).
Окно терминала
npm install -D vitest @vitest/ui
vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'node',
coverage: {
reporter: ['text', 'html'],
threshold: { lines: 80 }
}
}
})
package.json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
}
}

End-to-end тесты — самые сложные для написания и самые ценные. AI резко снижает порог входа.

Напиши Playwright E2E тест для такого сценария:
- Пользователь открывает /login
- Вводит email: [email protected], пароль: Password123
- Кликает "Войти"
- Должен увидеть дашборд с текстом "Добро пожаловать"
- Проверь что URL стал /dashboard
TypeScript, используй Page Object Model.

AI пишет:

e2e/auth.spec.ts
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 loginPage.login('[email protected]', 'Password123')
await expect(page).toHaveURL('/dashboard')
await expect(page.getByText('Добро пожаловать')).toBeVisible()
})
test('неверный пароль показывает ошибку', async ({ page }) => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.login('[email protected]', 'wrongpassword')
await expect(page.getByRole('alert')).toContainText('Неверный пароль')
await expect(page).toHaveURL('/login')
})
})
e2e/pages/LoginPage.ts
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 умеет записывать действия браузера и генерировать тесты. AI доводит их до ума:

Окно терминала
npx playwright codegen http://localhost:3000

Получил сырой тест? Отдай в AI:

Вот записанный Playwright тест (сырой).
Рефактори:
- Вынеси в Page Object Model
- Добавь осмысленные assertions
- Убери хрупкие селекторы, используй роли и лейблы
- Добавь тест для негативного сценария

Это суперсила. AI видит edge cases, которые ты пропустишь.

Вот функция [код].
Найди все возможные edge cases и для каждого:
1. Опиши проблему
2. Напиши тест который её поймает
3. Предложи фикс если нужен
Думай как опытный QA инженер.
КатегорияПримеры
Null/undefinednull, undefined, не передан аргумент
Пустые данные[], {}, "", 0
Граничные числаInfinity, NaN, Number.MAX_SAFE_INTEGER
СтрокиПустая строка, Unicode, эмодзи, HTML-инъекции
АсинхронностьTimeout, параллельные вызовы, race conditions
ТипыЧисло вместо строки, массив вместо объекта

Пример — функция парсинга JSON с AI-найденными edge cases:

utils/safeJson.test.ts
describe('safeParseJson', () => {
it('парсит валидный JSON', () => { /* ... */ })
it('возвращает null при невалидном JSON', () => { /* ... */ })
it('обрабатывает пустую строку', () => { /* ... */ })
it('обрабатывает null на входе', () => { /* ... */ })
it('парсит вложенные объекты', () => { /* ... */ })
it('обрабатывает числа и булевы значения', () => { /* ... */ })
it('не падает на очень длинных строках', () => { /* ... */ })
// ← последние 3 нашёл AI, не я
})

Прежде чем пушить — прогони через AI-ревью. Это работает лучше статических анализаторов.

Сделай code review этого кода с фокусом на:
1. Потенциальные баги (NPE, off-by-one, race conditions)
2. Уязвимости безопасности
3. Проблемы производительности
4. Места без error handling
5. Сложную логику которую стоит протестировать
Для каждой проблемы: покажи строку, объясни риск, предложи фикс.
// ❌ Проблемный код
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 может быть nulluser.id упадёт с TypeError
  • Строка 4: posts может быть null.slice() упадёт
  • Производительность: два последовательных запроса, лучше параллельно через Promise.all
  • Нет логирования ошибок
// ✅ После AI code review
async 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) }
}

Плагин для VS Code/JetBrains. Специализируется именно на тестах:

  • Анализирует функцию и предлагает тест-кейсы в сайдбаре
  • Показывает какие edge cases не покрыты
  • Интегрируется с Jest, Vitest, Pytest, JUnit
Окно терминала
# Установка в VS Code
# Extensions → искать "Qodo Gen" (бывший CodiumAI)

Copilot умеет генерировать тесты через /tests в чате:

# В Copilot Chat:
/tests # Генерирует тесты для выделенного кода
# Или с контекстом:
Write comprehensive tests for #file:utils/auth.ts
Focus on security edge cases

В 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 с дебаунсом.

components/SearchBar.tsx
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="Поисковая строка"
/>
)
}
Вот React компонент SearchBar с дебаунсом.
Напиши тесты с Vitest + React Testing Library + @testing-library/user-event.
Покрой:
- Рендер с дефолтными пропсами
- Рендер с кастомным placeholder
- onSearch вызывается после debounce
- onSearch НЕ вызывается сразу при вводе
- onSearch НЕ вызывается для пустой строки/пробелов
- Быстрый ввод нескольких символов — только один вызов
- Кастомный debounceMs работает корректно
Используй vi.useFakeTimers() для контроля времени.
components/SearchBar.test.tsx
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

1. Пишешь функцию/компонент
2. Просишь AI написать тесты (с edge cases!)
3. Запускаешь тесты — смотришь что упало
4. Просишь AI объяснить падение и предложить фикс
5. Перед PR — просишь AI сделать code review
6. Пушишь уверенно

С AI тесты перестают быть болью. Они становятся быстрее, полнее и интереснее. Попробуй один раз — потом не захочешь без них.