**Vitest** — это тестовый фреймворк, созданный командой Vite. Он совместим с API Jest, но работает значительно быстрее благодаря тому, что использует ту же конфигурацию и трансформацию, что и сам Vite. Не нужно настраивать отдельный TypeScript-транспайлер для тестов.
npm install -D vitest @vue/test-utils jsdom// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom', // имитирует браузерное окружение
globals: true, // describe/it/expect без импорта
setupFiles: ['./src/test/setup.ts'],
},
})// src/utils/format.test.ts
import { describe, it, expect } from 'vitest'
import { formatPrice, formatDate } from '@/utils/format'
describe('formatPrice', () => {
it('форматирует целое число', () => {
expect(formatPrice(1000)).toBe('1 000 ₽')
})
it('форматирует дробное число', () => {
expect(formatPrice(99.9)).toBe('99,90 ₽')
})
it('возвращает 0 ₽ при нулевом значении', () => {
expect(formatPrice(0)).toBe('0 ₽')
})
})// src/components/Counter.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from '@/components/Counter.vue'
describe('Counter', () => {
it('показывает начальное значение 0', () => {
const wrapper = mount(Counter)
expect(wrapper.text()).toContain('0')
})
it('увеличивает счётчик при нажатии кнопки', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('1')
})
it('принимает проп initialValue', () => {
const wrapper = mount(Counter, {
props: { initialValue: 10 },
})
expect(wrapper.text()).toContain('10')
})
})import { describe, it, expect, vi, beforeEach } from 'vitest'
import { fetchUser } from '@/api/users'
// Полностью заменяем модуль
vi.mock('@/api/users', () => ({
fetchUser: vi.fn(),
}))
describe('fetchUser', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('возвращает пользователя', async () => {
const mockUser = { id: 1, name: 'Alice' }
fetchUser.mockResolvedValueOnce(mockUser)
const result = await fetchUser(1)
expect(result).toEqual(mockUser)
expect(fetchUser).toHaveBeenCalledWith(1)
})
it('пробрасывает ошибку', async () => {
fetchUser.mockRejectedValueOnce(new Error('Not found'))
await expect(fetchUser(999)).rejects.toThrow('Not found')
})
})npm run test -- --coverage// vite.config.ts
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
exclude: ['node_modules/', 'src/test/'],
},
}После запуска откроется отчёт в coverage/index.html с визуальным отображением покрытых строк.
expect(value).toBe(42) // строгое равенство (===)
expect(obj).toEqual({ a: 1 }) // глубокое равенство
expect(arr).toContain('item') // массив содержит элемент
expect(str).toMatch(/pattern/) // строка по regexp
expect(fn).toThrow('error msg') // функция бросает ошибку
expect(spy).toHaveBeenCalled() // мок был вызван
expect(spy).toHaveBeenCalledTimes(2)
expect(promise).resolves.toBe(42) // промис резолвится в 42Мини-фреймворк тестирования: реализация describe/it/expect с матчерами — так работает Vitest под капотом
// Реализуем минимальный тестовый фреймворк,
// аналогичный Vitest/Jest, чтобы понять, как они работают.
// ============================================
// Матчеры (expect)
// ============================================
function expect(received) {
return {
toBe(expected) {
if (received !== expected) {
throw new Error(
`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(received)}`
)
}
},
toEqual(expected) {
const a = JSON.stringify(received)
const b = JSON.stringify(expected)
if (a !== b) {
throw new Error(`Expected ${b}, got ${a}`)
}
},
toContain(item) {
const found = Array.isArray(received)
? received.includes(item)
: String(received).includes(item)
if (!found) {
throw new Error(`Expected ${JSON.stringify(received)} to contain ${JSON.stringify(item)}`)
}
},
toThrow(msg) {
let threw = false
let error = null
try { received() } catch (e) { threw = true; error = e }
if (!threw) throw new Error('Expected function to throw, but it did not')
if (msg && !error.message.includes(msg)) {
throw new Error(`Expected error message to include "${msg}", got "${error.message}"`)
}
},
toBeTruthy() {
if (!received) throw new Error(`Expected truthy value, got ${received}`)
},
toBeFalsy() {
if (received) throw new Error(`Expected falsy value, got ${received}`)
},
resolves: {
async toBe(expected) {
const result = await received
if (result !== expected) {
throw new Error(`Promise resolved to ${result}, expected ${expected}`)
}
}
}
}
}
// ============================================
// describe / it / beforeEach
// ============================================
let stats = { passed: 0, failed: 0 }
let currentSuite = ''
const beforeEachFns = []
function describe(name, fn) {
currentSuite = name
console.log(`\n ${name}`)
fn()
currentSuite = ''
}
function it(name, fn) {
// Выполняем все beforeEach перед каждым тестом
for (const setup of beforeEachFns) setup()
try {
const result = fn()
// Обрабатываем асинхронные тесты
if (result && typeof result.then === 'function') {
result.then(() => {
stats.passed++
console.log(` ✓ ${name}`)
}).catch(err => {
stats.failed++
console.log(` ✗ ${name}: ${err.message}`)
})
} else {
stats.passed++
console.log(` ✓ ${name}`)
}
} catch (err) {
stats.failed++
console.log(` ✗ ${name}: ${err.message}`)
}
}
function beforeEach(fn) {
beforeEachFns.push(fn)
}
// ============================================
// vi.fn() — мок-функции
// ============================================
function vi_fn() {
const calls = []
let implementations = []
const mock = function(...args) {
calls.push(args)
if (implementations.length > 0) {
const impl = implementations.shift()
return impl(...args)
}
}
mock.calls = calls
mock.mockReturnValueOnce = (val) => {
implementations.push(() => val)
return mock
}
mock.mockResolvedValueOnce = (val) => {
implementations.push(() => Promise.resolve(val))
return mock
}
mock.toHaveBeenCalled = () => calls.length > 0
mock.toHaveBeenCalledWith = (...args) =>
calls.some(c => JSON.stringify(c) === JSON.stringify(args))
mock.mockClear = () => { calls.length = 0; implementations.length = 0 }
return mock
}
// ============================================
// Тестируем реальный код с нашим мини-фреймворком
// ============================================
// Тестируемые утилиты
function formatPrice(amount) {
if (amount === 0) return '0 ₽'
return amount.toLocaleString('ru-RU') + ' ₽'
}
function sum(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Оба аргумента должны быть числами')
}
return a + b
}
async function fetchData(id, fetcher) {
const data = await fetcher(id)
return { ...data, fetched: true }
}
// Тесты
console.log('=== Запуск тестов ===')
describe('formatPrice', () => {
it('форматирует число', () => {
const result = formatPrice(1000)
expect(result).toContain('₽')
})
it('обрабатывает ноль', () => {
expect(formatPrice(0)).toBe('0 ₽')
})
})
describe('sum', () => {
it('складывает два числа', () => {
expect(sum(2, 3)).toBe(5)
})
it('бросает ошибку для не-чисел', () => {
expect(() => sum('a', 1)).toThrow('числами')
})
})
describe('fetchData с моком', () => {
const mockFetcher = vi_fn()
it('добавляет поле fetched: true', async () => {
mockFetcher.mockResolvedValueOnce({ id: 1, name: 'Alice' })
const result = await fetchData(1, mockFetcher)
expect(result).toEqual({ id: 1, name: 'Alice', fetched: true })
})
})
setTimeout(() => {
console.log(`\n=== Итог: ${stats.passed} passed, ${stats.failed} failed ===`)
}, 100)**Vitest** — это тестовый фреймворк, созданный командой Vite. Он совместим с API Jest, но работает значительно быстрее благодаря тому, что использует ту же конфигурацию и трансформацию, что и сам Vite. Не нужно настраивать отдельный TypeScript-транспайлер для тестов.
npm install -D vitest @vue/test-utils jsdom// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom', // имитирует браузерное окружение
globals: true, // describe/it/expect без импорта
setupFiles: ['./src/test/setup.ts'],
},
})// src/utils/format.test.ts
import { describe, it, expect } from 'vitest'
import { formatPrice, formatDate } from '@/utils/format'
describe('formatPrice', () => {
it('форматирует целое число', () => {
expect(formatPrice(1000)).toBe('1 000 ₽')
})
it('форматирует дробное число', () => {
expect(formatPrice(99.9)).toBe('99,90 ₽')
})
it('возвращает 0 ₽ при нулевом значении', () => {
expect(formatPrice(0)).toBe('0 ₽')
})
})// src/components/Counter.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from '@/components/Counter.vue'
describe('Counter', () => {
it('показывает начальное значение 0', () => {
const wrapper = mount(Counter)
expect(wrapper.text()).toContain('0')
})
it('увеличивает счётчик при нажатии кнопки', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('1')
})
it('принимает проп initialValue', () => {
const wrapper = mount(Counter, {
props: { initialValue: 10 },
})
expect(wrapper.text()).toContain('10')
})
})import { describe, it, expect, vi, beforeEach } from 'vitest'
import { fetchUser } from '@/api/users'
// Полностью заменяем модуль
vi.mock('@/api/users', () => ({
fetchUser: vi.fn(),
}))
describe('fetchUser', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('возвращает пользователя', async () => {
const mockUser = { id: 1, name: 'Alice' }
fetchUser.mockResolvedValueOnce(mockUser)
const result = await fetchUser(1)
expect(result).toEqual(mockUser)
expect(fetchUser).toHaveBeenCalledWith(1)
})
it('пробрасывает ошибку', async () => {
fetchUser.mockRejectedValueOnce(new Error('Not found'))
await expect(fetchUser(999)).rejects.toThrow('Not found')
})
})npm run test -- --coverage// vite.config.ts
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
exclude: ['node_modules/', 'src/test/'],
},
}После запуска откроется отчёт в coverage/index.html с визуальным отображением покрытых строк.
expect(value).toBe(42) // строгое равенство (===)
expect(obj).toEqual({ a: 1 }) // глубокое равенство
expect(arr).toContain('item') // массив содержит элемент
expect(str).toMatch(/pattern/) // строка по regexp
expect(fn).toThrow('error msg') // функция бросает ошибку
expect(spy).toHaveBeenCalled() // мок был вызван
expect(spy).toHaveBeenCalledTimes(2)
expect(promise).resolves.toBe(42) // промис резолвится в 42Мини-фреймворк тестирования: реализация describe/it/expect с матчерами — так работает Vitest под капотом
// Реализуем минимальный тестовый фреймворк,
// аналогичный Vitest/Jest, чтобы понять, как они работают.
// ============================================
// Матчеры (expect)
// ============================================
function expect(received) {
return {
toBe(expected) {
if (received !== expected) {
throw new Error(
`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(received)}`
)
}
},
toEqual(expected) {
const a = JSON.stringify(received)
const b = JSON.stringify(expected)
if (a !== b) {
throw new Error(`Expected ${b}, got ${a}`)
}
},
toContain(item) {
const found = Array.isArray(received)
? received.includes(item)
: String(received).includes(item)
if (!found) {
throw new Error(`Expected ${JSON.stringify(received)} to contain ${JSON.stringify(item)}`)
}
},
toThrow(msg) {
let threw = false
let error = null
try { received() } catch (e) { threw = true; error = e }
if (!threw) throw new Error('Expected function to throw, but it did not')
if (msg && !error.message.includes(msg)) {
throw new Error(`Expected error message to include "${msg}", got "${error.message}"`)
}
},
toBeTruthy() {
if (!received) throw new Error(`Expected truthy value, got ${received}`)
},
toBeFalsy() {
if (received) throw new Error(`Expected falsy value, got ${received}`)
},
resolves: {
async toBe(expected) {
const result = await received
if (result !== expected) {
throw new Error(`Promise resolved to ${result}, expected ${expected}`)
}
}
}
}
}
// ============================================
// describe / it / beforeEach
// ============================================
let stats = { passed: 0, failed: 0 }
let currentSuite = ''
const beforeEachFns = []
function describe(name, fn) {
currentSuite = name
console.log(`\n ${name}`)
fn()
currentSuite = ''
}
function it(name, fn) {
// Выполняем все beforeEach перед каждым тестом
for (const setup of beforeEachFns) setup()
try {
const result = fn()
// Обрабатываем асинхронные тесты
if (result && typeof result.then === 'function') {
result.then(() => {
stats.passed++
console.log(` ✓ ${name}`)
}).catch(err => {
stats.failed++
console.log(` ✗ ${name}: ${err.message}`)
})
} else {
stats.passed++
console.log(` ✓ ${name}`)
}
} catch (err) {
stats.failed++
console.log(` ✗ ${name}: ${err.message}`)
}
}
function beforeEach(fn) {
beforeEachFns.push(fn)
}
// ============================================
// vi.fn() — мок-функции
// ============================================
function vi_fn() {
const calls = []
let implementations = []
const mock = function(...args) {
calls.push(args)
if (implementations.length > 0) {
const impl = implementations.shift()
return impl(...args)
}
}
mock.calls = calls
mock.mockReturnValueOnce = (val) => {
implementations.push(() => val)
return mock
}
mock.mockResolvedValueOnce = (val) => {
implementations.push(() => Promise.resolve(val))
return mock
}
mock.toHaveBeenCalled = () => calls.length > 0
mock.toHaveBeenCalledWith = (...args) =>
calls.some(c => JSON.stringify(c) === JSON.stringify(args))
mock.mockClear = () => { calls.length = 0; implementations.length = 0 }
return mock
}
// ============================================
// Тестируем реальный код с нашим мини-фреймворком
// ============================================
// Тестируемые утилиты
function formatPrice(amount) {
if (amount === 0) return '0 ₽'
return amount.toLocaleString('ru-RU') + ' ₽'
}
function sum(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Оба аргумента должны быть числами')
}
return a + b
}
async function fetchData(id, fetcher) {
const data = await fetcher(id)
return { ...data, fetched: true }
}
// Тесты
console.log('=== Запуск тестов ===')
describe('formatPrice', () => {
it('форматирует число', () => {
const result = formatPrice(1000)
expect(result).toContain('₽')
})
it('обрабатывает ноль', () => {
expect(formatPrice(0)).toBe('0 ₽')
})
})
describe('sum', () => {
it('складывает два числа', () => {
expect(sum(2, 3)).toBe(5)
})
it('бросает ошибку для не-чисел', () => {
expect(() => sum('a', 1)).toThrow('числами')
})
})
describe('fetchData с моком', () => {
const mockFetcher = vi_fn()
it('добавляет поле fetched: true', async () => {
mockFetcher.mockResolvedValueOnce({ id: 1, name: 'Alice' })
const result = await fetchData(1, mockFetcher)
expect(result).toEqual({ id: 1, name: 'Alice', fetched: true })
})
})
setTimeout(() => {
console.log(`\n=== Итог: ${stats.passed} passed, ${stats.failed} failed ===`)
}, 100)Реализуй функцию `runTests(testSuite)`, которая принимает массив тест-объектов вида `{ name: string, fn: () => void }` и возвращает объект `{ passed: number, failed: number, results: Array<{name, status, error}> }`. Каждая функция `fn` может бросить исключение (тест провален) или выполниться без ошибок (тест прошёл). Также напиши функцию `assertEqual(a, b)` — если `a !== b`, бросает `Error` с сообщением о несоответствии.
В runTests используй цикл for...of и оберни вызов fn() в try { fn(); results.push({name, status: "passed"}) } catch(e) { results.push({name, status: "failed", error: e.message}); failed++ }. Счётчик passed увеличивай в блоке try после успешного fn().
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке