vitest для TypeScript: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

vitest для TypeScript: Полное руководство

vitest — это современный, быстрый и мощный фреймворк для модульного тестирования, разработанный специально для экосистемы TypeScript и JavaScript. Он создан как прямая альтернатива Jest, но с фокусом на скорость выполнения и нативную поддержку Vite. Если вы используете Vite для сборки проекта, vitest станет идеальным выбором, так как он использует тот же плагин и конфигурацию, что и ваш основной инструмент сборки.

Зачем нужен vitest? Во-первых, он невероятно быстр благодаря использованию esbuild для транспиляции и нативной поддержке ESM (ECMAScript Modules). Во-вторых, он предлагает богатый API, совместимый с Jest, что упрощает миграцию. В-третьих, vitest предоставляет встроенную поддержку TypeScript, JSX, mocking, coverage и snapshot-тестирования без необходимости установки дополнительных пакетов. Это делает его идеальным инструментом как для небольших проектов, так и для крупных enterprise-приложений.

На этом образовательном сайте PythonLib мы покажем, как использовать vitest для тестирования TypeScript-кода. Вы узнаете, как установить библиотеку, написать первые тесты, освоить основные методы и избежать типичных ошибок. К концу статьи вы сможете уверенно использовать vitest в своих проектах.

Установка

Установка vitest осуществляется через npm или yarn. Для TypeScript-проекта вам также потребуется установить сам TypeScript и типы для Node.js (если вы тестируете серверный код).

npm install -D vitest typescript @types/node

Если вы используете yarn:

yarn add -D vitest typescript @types/node

После установки добавьте скрипт в package.json:

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run"
  }
}

Команда vitest запускает тесты в watch-режиме (автоматический перезапуск при изменениях), а vitest run — однократный прогон.

Быстрый старт

Создайте файл sum.ts с простой функцией:

// sum.ts
export function sum(a: number, b: number): number {
  return a + b;
}

Теперь создайте файл теста sum.test.ts:

// sum.test.ts
import { describe, it, expect } from 'vitest';
import { sum } from './sum';

describe('sum function', () => {
  it('should add two numbers correctly', () => {
    expect(sum(2, 3)).toBe(5);
  });

  it('should handle negative numbers', () => {
    expect(sum(-1, -2)).toBe(-3);
  });
});

Запустите тесты:

npm run test

Вы увидите зеленый вывод с сообщением об успешном прохождении тестов. Поздравляем, вы написали свой первый тест на vitest!

Основные методы и функции

vitest предоставляет богатый API для написания тестов. Ниже приведены 8 ключевых методов с описанием и примерами.

1. describe

Сигнатура: describe(name: string, fn: () => void): void

Описание: Группирует связанные тесты в блок. Улучшает читаемость и позволяет применять хуки (beforeEach, afterEach) к группе.

import { describe, it, expect } from 'vitest';

describe('Math operations', () => {
  it('should add', () => {
    expect(1 + 1).toBe(2);
  });

  it('should subtract', () => {
    expect(5 - 3).toBe(2);
  });
});

2. it / test

Сигнатура: it(name: string, fn: () => void | Promise<void>, timeout?: number): void

Описание: Определяет отдельный тест. Можно использовать как it, так и test — это синонимы.

import { it, expect } from 'vitest';

it('should return true', () => {
  expect(true).toBe(true);
});

// Асинхронный тест
it('should resolve with value', async () => {
  const result = await Promise.resolve(42);
  expect(result).toBe(42);
});

3. expect

Сигнатура: expect(value: any): Assertion

Описание: Основная функция для создания утверждений (assertions). Возвращает объект с методами-матчерами, такими как toBe, toEqual, toBeTruthy и другими.

import { expect } from 'vitest';

const obj = { a: 1, b: 2 };
const arr = [1, 2, 3];

expect(1 + 1).toBe(2);           // строгое равенство (===)
expect(obj).toEqual({ a: 1, b: 2 }); // глубокое равенство
expect(arr).toContain(2);        // проверка наличия элемента
expect(null).toBeNull();         // проверка на null
expect(undefined).toBeUndefined(); // проверка на undefined

4. beforeEach / afterEach

Сигнатура: beforeEach(fn: () => void | Promise<void>): void

Описание: Хуки, которые выполняются перед каждым тестом (или после) в текущем блоке describe. Полезны для подготовки данных или очистки.

import { describe, it, expect, beforeEach } from 'vitest';

let counter: number;

beforeEach(() => {
  counter = 0;
});

describe('Counter', () => {
  it('should start at 0', () => {
    expect(counter).toBe(0);
  });

  it('should increment', () => {
    counter++;
    expect(counter).toBe(1);
  });
});

5. vi.fn

Сигнатура: vi.fn(impl?: (...args: any[]) => any): Mock

Описание: Создает mock-функцию (заглушку). Можно задать реализацию по умолчанию или возвращаемое значение. Используется для изоляции тестируемого кода.

import { vi, it, expect } from 'vitest';

it('should call mock function', () => {
  const mockFn = vi.fn((x: number) => x * 2);

  const result = mockFn(5);

  expect(mockFn).toHaveBeenCalledOnce();
  expect(mockFn).toHaveBeenCalledWith(5);
  expect(result).toBe(10);
});

6. vi.spyOn

Сигнатура: vi.spyOn(object: object, method: string): Mock

Описание: Создает mock для существующего метода объекта. Позволяет отслеживать вызовы и изменять поведение метода.

import { vi, it, expect } from 'vitest';

const calculator = {
  add: (a: number, b: number) => a + b,
};

it('should spy on add method', () => {
  const spy = vi.spyOn(calculator, 'add');

  const result = calculator.add(2, 3);

  expect(spy).toHaveBeenCalledWith(2, 3);
  expect(result).toBe(5);

  spy.mockRestore(); // восстановить оригинальную реализацию
});

7. vi.mock

Сигнатура: vi.mock(path: string, factory?: () => object): void

Описание: Автоматически подменяет весь модуль на mock-версию. Это глобальная операция, которая применяется ко всем тестам в файле.

// userService.ts
export const getUser = (id: number) => {
  return { id, name: 'Real User' };
};

// userService.test.ts
import { vi, it, expect } from 'vitest';

vi.mock('./userService', () => ({
  getUser: vi.fn(() => ({ id: 1, name: 'Mocked User' })),
}));

import { getUser } from './userService';

it('should return mocked user', () => {
  const user = getUser(1);
  expect(user.name).toBe('Mocked User');
});

8. vi.useFakeTimers / vi.useRealTimers

Сигнатура: vi.useFakeTimers(): void / vi.useRealTimers(): void

Описание: Позволяет управлять временем в тестах. useFakeTimers заменяет setTimeout, setInterval и Date на управляемые версии. Полезно для тестирования таймеров и асинхронных операций.

import { vi, it, expect, afterEach } from 'vitest';

afterEach(() => {
  vi.useRealTimers();
});

it('should advance timers', () => {
  vi.useFakeTimers();

  const fn = vi.fn();
  setTimeout(fn, 1000);

  vi.advanceTimersByTime(1000); // перемещаем время на 1 секунду

  expect(fn).toHaveBeenCalled();
});

Реальный практический пример

Рассмотрим пример тестирования небольшого сервиса для работы с задачами (TodoService). Мы протестируем CRUD-операции, используя mock для внешнего хранилища.

// todoService.ts
export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

export interface Storage {
  getTodos(): Promise;
  saveTodos(todos: Todo[]): Promise;
}

export class TodoService {
  private todos: Todo[] = [];
  private storage: Storage;

  constructor(storage: Storage) {
    this.storage = storage;
  }

  async load(): Promise {
    this.todos = await this.storage.getTodos();
  }

  async add(title: string): Promise {
    const newTodo: Todo = {
      id: Date.now(),
      title,
      completed: false,
    };
    this.todos.push(newTodo);
    await this.storage.saveTodos(this.todos);
    return newTodo;
  }

  async toggle(id: number): Promise {
    const todo = this.todos.find(t => t.id === id);
    if (todo) {
      todo.completed = !todo.completed;
      await this.storage.saveTodos(this.todos);
    }
    return todo;
  }

  async remove(id: number): Promise {
    const index = this.todos.findIndex(t => t.id === id);
    if (index !== -1) {
      this.todos.splice(index, 1);
      await this.storage.saveTodos(this.todos);
      return true;
    }
    return false;
  }

  getAll(): Todo[] {
    return this.todos;
  }
}

Теперь напишем тесты для этого сервиса:

// todoService.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TodoService, Storage } from './todoService';

describe('TodoService', () => {
  let service: TodoService;
  let mockStorage: Storage;

  beforeEach(() => {
    // Создаем mock для хранилища
    mockStorage = {
      getTodos: vi.fn().mockResolvedValue([]),
      saveTodos: vi.fn().mockResolvedValue(undefined),
    };
    service = new TodoService(mockStorage);
  });

  it('should load todos from storage', async () => {
    const mockTodos = [
      { id: 1, title: 'Test', completed: false },
    ];
    mockStorage.getTodos = vi.fn().mockResolvedValue(mockTodos);

    await service.load();

    expect(service.getAll()).toEqual(mockTodos);
    expect(mockStorage.getTodos).toHaveBeenCalledOnce();
  });

  it('should add a new todo', async () => {
    await service.load();
    const newTodo = await service.add('New Task');

    expect(newTodo.title).toBe('New Task');
    expect(newTodo.completed).toBe(false);
    expect(service.getAll()).toHaveLength(1);
    expect(mockStorage.saveTodos).toHaveBeenCalledWith(
      expect.arrayContaining([expect.objectContaining({ title: 'New Task' })])
    );
  });

  it('should toggle todo completion', async () => {
    await service.load();
    const added = await service.add('Task to toggle');
    const toggled = await service.toggle(added.id);

    expect(toggled?.completed).toBe(true);

    const toggledAgain = await service.toggle(added.id);
    expect(toggledAgain?.completed).toBe(false);
  });

  it('should remove a todo', async () => {
    await service.load();
    await service.add('Task to remove');
    const added = service.getAll()[0];

    const result = await service.remove(added.id);

    expect(result).toBe(true);
    expect(service.getAll()).toHaveLength(0);
    expect(mockStorage.saveTodos).toHaveBeenCalledWith([]);
  });

  it('should return false when removing non-existent todo', async () => {
    await service.load();
    const result = await service.remove(999);

    expect(result).toBe(false);
  });

  it('should handle storage errors gracefully', async () => {
    mockStorage.getTodos = vi.fn().mockRejectedValue(new Error('Storage error'));

    await expect(service.load()).rejects.toThrow('Storage error');
  });
});

Этот пример демонстрирует использование describe, it, expect, beforeEach, vi.fn и работу с асинхронными тестами.

Советы по производительности и типичные ошибки

Советы по производительности

  • Используйте vitest run для CI/CD. Watch-режим потребляет больше ресурсов, поэтому для однократного прогона в пайплайнах используйте vitest run.
  • Избегайте глобальных моков без необходимости. vi.mock на уровне модуля влияет на все тесты в файле. Используйте vi.fn или vi.spyOn для локальных подмен.
  • Настройте pool в конфигурации. Для больших проектов используйте pool: 'forks' (вместо threads) для изоляции тестов и

Рекомендаций