Подходы к тестированию проектов на Vite

Архитектура Vite существенно отличается от классических сборщиков вроде Webpack. Во время разработки используется нативная работа браузера с ES-модулями, а продакшн-сборка выполняется через Rollup. Такая модель влияет на способы тестирования, скорость запуска тестов, обработку модулей, мокирование зависимостей и интеграцию инструментов.

Тестирование в проектах на Vite обычно строится вокруг нескольких уровней:

  • модульное тестирование;
  • компонентное тестирование;
  • интеграционное тестирование;
  • e2e-тестирование;
  • визуальное тестирование;
  • тестирование SSR;
  • тестирование производительности.

Основным инструментом тестирования в экосистеме Vite стал Vitest — тестовый раннер, созданный специально под архитектуру Vite.


Особенности тестирования в Vite

Главная особенность заключается в том, что Vite уже умеет:

  • быстро резолвить ES-модули;
  • трансформировать TypeScript;
  • работать с JSX и TSX;
  • использовать плагины;
  • поддерживать alias;
  • кэшировать зависимости.

Vitest использует эти возможности напрямую, благодаря чему:

  • тесты запускаются быстрее;
  • не требуется отдельная сложная конфигурация;
  • поддерживается HMR-подобное обновление тестов;
  • уменьшается дублирование настроек между приложением и тестовой средой.

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

Базовая конфигурация Vitest

Минимальная настройка в 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

Группировка тестов:

describe('math utils', () => {

})

test

Отдельный тест:

test('adds numbers', () => {
    expect(1 + 2).toBe(3)
})

expect

Проверка результата:

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/await

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')
})

Использование beforeEach и afterEach

Подготовка окружения

let users

beforeEach(() => {
    users = []
})

Очистка

afterEach(() => {
    localStorage.clear()
})

Мокирование функций

Vitest предоставляет API, совместимый с Jest.

vi.fn

const callback = vi.fn()

callback()
callback()

expect(callback).toHaveBeenCalledTimes(2)

Мокирование модулей

vi.mock

vi.mock('./api', () => {
    return {
        getUsers: vi.fn(() => Promise.resolve([]))
    }
})

Проверка вызовов

const fn = vi.fn()

fn('hello')

expect(fn).toHaveBeenCalledWith('hello')

Spy-функции

vi.spyOn

const spy = vi.spyOn(console, 'log')

console.log('test')

expect(spy).toHaveBeenCalled()

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

Для браузерного интерфейса обычно используется Testing Library.

Пример DOM-теста

import { screen } from '@testing-library/dom'

document.body.innerHTML = `
    <button>Save</button>
`

test('button exists', () => {
    const button = screen.getByText('Save')

    expect(button).toBeTruthy()
})

Тестирование React-компонентов

Компонент

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()
})

Тестирование Vue-компонентов

Компонент

<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-тестирование

Snapshot позволяет сохранять структуру вывода компонента.

test('matches snapshot', () => {
    const result = {
        id: 1,
        title: 'Post'
    }

    expect(result).toMatchSnapshot()
})

После первого запуска создаётся snapshot-файл.


Когда snapshot-тесты полезны

Подход особенно эффективен:

  • для UI-компонентов;
  • сериализуемых объектов;
  • HTML-структур;
  • JSON-ответов API.

Недостатки snapshot-тестирования

Основные проблемы:

  • snapshots быстро устаревают;
  • большие snapshots сложно анализировать;
  • возможно случайное подтверждение ошибочных изменений;
  • ухудшается читаемость тестов.

Поэтому 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()
})

Изоляция тестов

Тесты не должны влиять друг на друга.

Для этого:

  • очищаются mock-функции;
  • удаляются временные данные;
  • пересоздаётся DOM;
  • изолируются глобальные состояния.

Настройка:

test: {
    clearMocks: true,
    restoreMocks: true
}

Тестирование API-запросов

Mock fetch

global.fetch = vi.fn(() =>
    Promise.resolve({
        json: () => Promise.resolve([
            { id: 1 }
        ])
    })
)

Использование MSW

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-приложений.


Тестирование composables и hooks

React hook

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)
})

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

Store

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)
})

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

Vite активно используется в SSR-фреймворках:

  • Nuxt
  • SvelteKit
  • Astro

При SSR-тестировании проверяются:

  • серверный рендеринг;
  • гидратация;
  • совместимость с Node.js;
  • отсутствие browser-only API;
  • корректность маршрутизации.

Проверка гидратации

Типичная проблема SSR:

window.localStorage

Такой код ломается на сервере.

Правильный вариант:

if (typeof window !== 'undefined') {
    window.localStorage.setItem('theme', 'dark')
}

Тесты SSR помогают обнаруживать подобные ошибки заранее.


End-to-End тестирование

Для e2e обычно используются:

  • Playwright
  • Cypress

Playwright и Vite

Установка

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()
})

Преимущества Playwright

Основные преимущества:

  • мультибраузерность;
  • параллельный запуск;
  • автоматические ожидания;
  • изоляция браузерных контекстов;
  • поддержка mobile-эмуляции;
  • трассировка тестов;
  • видео и скриншоты.

Cypress и Vite

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')
    })
})

Визуальное тестирование

Визуальные тесты помогают выявлять:

  • смещения элементов;
  • поломку верстки;
  • изменение цветов;
  • проблемы адаптивности;
  • ошибки темы оформления.

Часто используются:

  • Percy;
  • Chromatic;
  • Playwright screenshots.

Тестирование производительности

Для Vite-проектов важны:

  • размер бандла;
  • скорость загрузки;
  • время гидратации;
  • эффективность code splitting;
  • время HMR.

Проверка размера бандла

Инструменты:

  • rollup-plugin-visualizer;
  • vite-bundle-visualizer;
  • source-map-explorer.

Покрытие кода тестами

Vitest поддерживает coverage через V8.

Конфигурация

test: {
    coverage: {
        provider: 'v8',
        reporter: ['text', 'html']
    }
}

Основные метрики coverage

Statements

Покрытие инструкций.

Branches

Покрытие ветвлений.

Functions

Покрытие функций.

Lines

Покрытие строк.


Почему 100% coverage не гарантирует качество

Даже при полном покрытии возможно:

  • отсутствие проверки бизнес-логики;
  • слабые assertions;
  • неправильные сценарии;
  • отсутствие edge-case тестов.

Coverage показывает лишь степень выполнения кода, но не качество тестирования.


Edge-case тестирование

Необходимо проверять:

  • пустые значения;
  • null и undefined;
  • большие массивы;
  • отрицательные числа;
  • сетевые ошибки;
  • таймауты;
  • race conditions;
  • нестандартный ввод.

Тестирование env-переменных

Vite использует import.meta.env.

Пример:

const api = import.meta.env.VITE_API_URL

В тестах:

vi.stubEnv('VITE_API_URL', 'http://localhost')

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

Если используются alias:

resolve: {
    alias: {
        '@': '/src'
    }
}

Vitest автоматически использует настройки Vite.

Дополнительная конфигурация обычно не требуется.


Fake timers

Использование таймеров

vi.useFakeTimers()

test('timer works', () => {
    const fn = vi.fn()

    setTimeout(fn, 1000)

    vi.advanceTimersByTime(1000)

    expect(fn).toHaveBeenCalled()
})

Параллельный запуск тестов

Vitest умеет выполнять тесты параллельно:

test.concurrent('parallel test', async () => {

})

Это особенно важно для больших проектов.


Watch-режим

Во время разработки:

vitest --watch

Vitest перезапускает только тесты, связанные с изменёнными модулями.

Это значительно ускоряет feedback loop.


UI-режим Vitest

Vitest поддерживает браузерный интерфейс:

vitest --ui

В интерфейсе доступны:

  • список тестов;
  • результаты;
  • ошибки;
  • stack traces;
  • coverage;
  • повторный запуск тестов.

CI/CD и Vite

Тесты обычно запускаются:

  • в GitHub Actions;
  • GitLab CI;
  • Jenkins;
  • Azure Pipelines.

Пример GitHub Actions

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

Стратегии тестирования

Pyramid Testing

Наиболее распространённая модель:

  • много unit-тестов;
  • меньше интеграционных;
  • минимальное число e2e.

Unit-тесты

Преимущества:

  • высокая скорость;
  • простота;
  • изоляция;
  • лёгкая отладка.

Недостаток — невозможность проверить поведение всей системы.


Интеграционные тесты

Проверяют взаимодействие:

  • компонентов;
  • API;
  • состояния;
  • маршрутизации.

Обычно дают больше практической пользы, чем избыточные unit-тесты.


E2E-тесты

Проверяют приложение полностью:

  • UI;
  • роутинг;
  • сервер;
  • авторизацию;
  • формы;
  • реальные браузеры.

Недостатки:

  • медленный запуск;
  • нестабильность;
  • сложная поддержка.

Распространённые ошибки тестирования

Избыточное мокирование

Слишком большое количество mock-объектов делает тесты искусственными.


Проверка реализации вместо поведения

Плохой подход:

expect(component.state.loading)
    .toBe(false)

Хороший подход:

expect(
    screen.getByText('Loaded')
).toBeVisible()

Хрупкие селекторы

Плохой пример:

document.querySelector('.btn-2')

Лучше использовать:

screen.getByRole('button')

Зависимость тестов друг от друга

Каждый тест должен запускаться независимо.


Рекомендуемая архитектура тестирования Vite-проекта

Типичная структура:

src/
├── components/
├── composables/
├── services/
├── stores/
├── pages/

tests/
├── unit/
├── integration/
├── e2e/
└── fixtures/

Подход Test Driven Development

TDD включает цикл:

  1. написание теста;
  2. получение ошибки;
  3. реализация;
  4. рефакторинг.

Vitest хорошо подходит для TDD благодаря высокой скорости запуска.


Особенности миграции с Jest

Vitest совместим с Jest API:

  • describe;
  • test;
  • expect;
  • mock-функции;
  • snapshots.

Однако имеются отличия:

  • ES-модули вместо CommonJS;
  • другая система трансформации;
  • иная работа hoisting;
  • интеграция с Vite-плагинами.

Тестирование TypeScript-проектов

Vitest умеет работать с TypeScript без дополнительной настройки.

Пример:

function sum(a: number, b: number): number {
    return a + b
}

Тест:

test('sum works', () => {
    expect(sum(1, 2)).toBe(3)
})

Browser Mode в Vitest

Vitest поддерживает запуск тестов непосредственно в браузере.

Это позволяет:

  • тестировать реальные browser API;
  • проверять CSS;
  • работать с layout;
  • тестировать Canvas;
  • проверять Web Components.

Тестирование Web Components

Пример:

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-структурах:

  • Turborepo;
  • Nx;
  • pnpm workspaces.

Vitest поддерживает:

  • workspace-конфигурации;
  • независимые проекты;
  • shared setup;
  • кэширование результатов.

Setup-файлы

Общая настройка:

test: {
    setupFiles: './tests/setup.js'
}

Пример setup:

import '@testing-library/jest-dom'

Debugging тестов

Полезные инструменты:

  • screen.debug();
  • Vitest UI;
  • browser devtools;
  • Playwright trace viewer;
  • console tracing.

Практический подход к тестированию Vite-приложений

Наиболее эффективной считается комбинация:

  • unit-тестов для бизнес-логики;
  • интеграционных тестов для UI;
  • ограниченного числа e2e-сценариев;
  • визуальных тестов для критичных интерфейсов;
  • coverage-контроля без фанатизма.

Архитектура Vite делает такой подход особенно удобным благодаря высокой скорости пересборки, нативной поддержке ES-модулей и тесной интеграции Vitest с системой разработки.