Архитектура Vite существенно отличается от классических сборщиков вроде Webpack. Во время разработки используется нативная работа браузера с ES-модулями, а продакшн-сборка выполняется через Rollup. Такая модель влияет на способы тестирования, скорость запуска тестов, обработку модулей, мокирование зависимостей и интеграцию инструментов.
Тестирование в проектах на Vite обычно строится вокруг нескольких уровней:
Основным инструментом тестирования в экосистеме Vite стал Vitest — тестовый раннер, созданный специально под архитектуру Vite.
Главная особенность заключается в том, что Vite уже умеет:
Vitest использует эти возможности напрямую, благодаря чему:
npm install -D vitest
Для тестирования DOM-приложений часто дополнительно устанавливаются:
npm install -D jsdom @testing-library/dom
Для React:
npm install -D @testing-library/react
Для Vue:
npm install -D @vue/test-utils
Минимальная настройка в vite.config.js:
import { defineConfig } from 'vite'
export default defineConfig({
test: {
globals: true,
environment: 'node'
}
})
Пример с DOM-средой:
import { defineConfig } from 'vite'
export default defineConfig({
test: {
globals: true,
environment: 'jsdom'
}
})
Распространённые варианты:
src/
├── components/
│ ├── Button.jsx
│ └── Button.test.jsx
или:
tests/
├── unit/
├── integration/
└── e2e/
Vitest автоматически ищет файлы:
*.test.js
*.spec.js
*.test.ts
*.spec.ts
Группировка тестов:
describe('math utils', () => {
})
Отдельный тест:
test('adds numbers', () => {
expect(1 + 2).toBe(3)
})
Проверка результата:
expect(result).toEqual(data)
Пример:
export function sum(a, b) {
return a + b
}
Тест:
import { sum } from './sum'
test('sum works correctly', () => {
expect(sum(2, 3)).toBe(5)
})
async function getUser() {
return {
id: 1,
name: 'Alex'
}
}
test('returns user', async () => {
const user = await getUser()
expect(user.name).toBe('Alex')
})
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero')
}
return a / b
}
test('throws error', () => {
expect(() => divide(10, 0))
.toThrow('Division by zero')
})
let users
beforeEach(() => {
users = []
})
afterEach(() => {
localStorage.clear()
})
Vitest предоставляет API, совместимый с Jest.
const callback = vi.fn()
callback()
callback()
expect(callback).toHaveBeenCalledTimes(2)
vi.mock('./api', () => {
return {
getUsers: vi.fn(() => Promise.resolve([]))
}
})
const fn = vi.fn()
fn('hello')
expect(fn).toHaveBeenCalledWith('hello')
const spy = vi.spyOn(console, 'log')
console.log('test')
expect(spy).toHaveBeenCalled()
Для браузерного интерфейса обычно используется Testing Library.
import { screen } from '@testing-library/dom'
document.body.innerHTML = `
<button>Save</button>
`
test('button exists', () => {
const button = screen.getByText('Save')
expect(button).toBeTruthy()
})
export function Button() {
return (
<button>Submit</button>
)
}
import { render, screen } from '@testing-library/react'
import { Button } from './Button'
test('renders button', () => {
render(<Button />)
expect(
screen.getByText('Submit')
).toBeInTheDocument()
})
import userEvent from '@testing-library/user-event'
test('click works', async () => {
const onCl ick = vi.fn()
render(
<button onCl ick={onClick}>
Click
</button>
)
await userEvent.click(
screen.getByText('Click')
)
expect(onClick).toHaveBeenCalled()
})
<script setup>
defineProps({
title: String
})
</script>
<template>
<h1>{{ title }}</h1>
</template>
import { mount } from '@vue/test-utils'
import Component from './Component.vue'
test('renders title', () => {
const wrapper = mount(Component, {
props: {
title: 'Hello'
}
})
expect(wrapper.text())
.toContain('Hello')
})
Snapshot позволяет сохранять структуру вывода компонента.
test('matches snapshot', () => {
const result = {
id: 1,
title: 'Post'
}
expect(result).toMatchSnapshot()
})
После первого запуска создаётся snapshot-файл.
Подход особенно эффективен:
Основные проблемы:
Поэтому snapshot-тесты рекомендуется использовать ограниченно.
Интеграционные тесты проверяют взаимодействие нескольких частей системы.
Пример:
test('user login flow', async () => {
render(<LoginForm />)
await userEvent.type(
screen.getByLabelText('Email'),
'admin@test.com'
)
await userEvent.type(
screen.getByLabelText('Password'),
'123456'
)
await userEvent.click(
screen.getByText('Login')
)
expect(
await screen.findByText('Dashboard')
).toBeInTheDocument()
})
Тесты не должны влиять друг на друга.
Для этого:
Настройка:
test: {
clearMocks: true,
restoreMocks: true
}
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve([
{ id: 1 }
])
})
)
Mock Service Worker позволяет эмулировать сервер.
import { rest } from 'msw'
export const handlers = [
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.json([
{ id: 1, name: 'John' }
])
)
})
]
MSW считается одним из наиболее надёжных подходов для интеграционного тестирования frontend-приложений.
function useCounter() {
const [count, setCount] = useState(0)
return {
count,
increment: () => setCount(v => v + 1)
}
}
Тест:
import { renderHook, act } from '@testing-library/react'
test('increments counter', () => {
const { result } = renderHook(() => useCounter())
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
})
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++
}
}
})
test('increments store value', () => {
const store = useCounterStore()
store.increment()
expect(store.count).toBe(1)
})
Vite активно используется в SSR-фреймворках:
При SSR-тестировании проверяются:
Типичная проблема SSR:
window.localStorage
Такой код ломается на сервере.
Правильный вариант:
if (typeof window !== 'undefined') {
window.localStorage.setItem('theme', 'dark')
}
Тесты SSR помогают обнаруживать подобные ошибки заранее.
Для e2e обычно используются:
npm install -D @playwright/test
import { test, expect } from '@playwright/test'
test('homepage works', async ({ page }) => {
await page.goto('http://localhost:5173')
await expect(
page.getByText('Welcome')
).toBeVisible()
})
Основные преимущества:
Cypress проще в освоении и хорошо подходит для frontend-команд.
Пример:
describe('login', () => {
it('works correctly', () => {
cy.visit('/')
cy.get('input[type=email]')
.type('admin@test.com')
cy.get('button')
.click()
cy.contains('Dashboard')
})
})
Визуальные тесты помогают выявлять:
Часто используются:
Для Vite-проектов важны:
Инструменты:
Vitest поддерживает coverage через V8.
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html']
}
}
Покрытие инструкций.
Покрытие ветвлений.
Покрытие функций.
Покрытие строк.
Даже при полном покрытии возможно:
Coverage показывает лишь степень выполнения кода, но не качество тестирования.
Необходимо проверять:
Vite использует import.meta.env.
Пример:
const api = import.meta.env.VITE_API_URL
В тестах:
vi.stubEnv('VITE_API_URL', 'http://localhost')
Если используются alias:
resolve: {
alias: {
'@': '/src'
}
}
Vitest автоматически использует настройки Vite.
Дополнительная конфигурация обычно не требуется.
vi.useFakeTimers()
test('timer works', () => {
const fn = vi.fn()
setTimeout(fn, 1000)
vi.advanceTimersByTime(1000)
expect(fn).toHaveBeenCalled()
})
Vitest умеет выполнять тесты параллельно:
test.concurrent('parallel test', async () => {
})
Это особенно важно для больших проектов.
Во время разработки:
vitest --watch
Vitest перезапускает только тесты, связанные с изменёнными модулями.
Это значительно ускоряет feedback loop.
Vitest поддерживает браузерный интерфейс:
vitest --ui
В интерфейсе доступны:
Тесты обычно запускаются:
name: tests
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install
- run: npm run test
Наиболее распространённая модель:
Преимущества:
Недостаток — невозможность проверить поведение всей системы.
Проверяют взаимодействие:
Обычно дают больше практической пользы, чем избыточные unit-тесты.
Проверяют приложение полностью:
Недостатки:
Слишком большое количество mock-объектов делает тесты искусственными.
Плохой подход:
expect(component.state.loading)
.toBe(false)
Хороший подход:
expect(
screen.getByText('Loaded')
).toBeVisible()
Плохой пример:
document.querySelector('.btn-2')
Лучше использовать:
screen.getByRole('button')
Каждый тест должен запускаться независимо.
Типичная структура:
src/
├── components/
├── composables/
├── services/
├── stores/
├── pages/
tests/
├── unit/
├── integration/
├── e2e/
└── fixtures/
TDD включает цикл:
Vitest хорошо подходит для TDD благодаря высокой скорости запуска.
Vitest совместим с Jest API:
Однако имеются отличия:
Vitest умеет работать с TypeScript без дополнительной настройки.
Пример:
function sum(a: number, b: number): number {
return a + b
}
Тест:
test('sum works', () => {
expect(sum(1, 2)).toBe(3)
})
Vitest поддерживает запуск тестов непосредственно в браузере.
Это позволяет:
Пример:
class MyElement extends HTMLElement {
connectedCallback() {
this.innerHTML = '<button>OK</button>'
}
}
customElements.define('my-element', MyElement)
Тест:
test('renders web component', () => {
document.body.innerHTML =
'<my-element></my-element>'
const button =
document.querySelector('button')
expect(button.textContent)
.toBe('OK')
})
Vite часто используется в monorepo-структурах:
Vitest поддерживает:
Общая настройка:
test: {
setupFiles: './tests/setup.js'
}
Пример setup:
import '@testing-library/jest-dom'
Полезные инструменты:
screen.debug();Наиболее эффективной считается комбинация:
Архитектура Vite делает такой подход особенно удобным благодаря высокой скорости пересборки, нативной поддержке ES-модулей и тесной интеграции Vitest с системой разработки.