Mocking (Мокирование) в Jest и Vitest
Что такое Mocking?
Mocking — это замена реального объекта/функции на подделку для изоляции при тестировании.
Jest Mocks
jest.fn() - Mock функция
typescript
const mockFn = jest.fn();
// Вызываем функцию
mockFn('hello', 123);
mockFn({ id: 1 });
// Проверяем вызовы
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('hello', 123);
expect(mockFn).toHaveBeenNthCalledWith(1, 'hello', 123);
// Проверяем возвращаемые значения
expect(mockFn).toHaveReturnedTimes(2);
expect(mockFn.mock.results).toHaveLength(2);Контроль возвращаемого значения
typescript
const mockFn = jest.fn();
// Возвращать одно значение
mockFn.mockReturnValue('default');
expect(mockFn()).toBe('default');
expect(mockFn()).toBe('default');
// Возвращать разные значения при разных вызовах
mockFn
.mockReturnValueOnce('first')
.mockReturnValueOnce('second')
.mockReturnValue('default');
expect(mockFn()).toBe('first');
expect(mockFn()).toBe('second');
expect(mockFn()).toBe('default');Mock с реализацией
typescript
const mockFn = jest.fn((x) => x * 2);
expect(mockFn(5)).toBe(10);
expect(mockFn).toHaveBeenCalledWith(5);
// Переписать реализацию
mockFn.mockImplementation((x) => x + 10);
expect(mockFn(5)).toBe(15);Mock Promise
typescript
const mockFn = jest.fn();
// Успешное разрешение
mockFn.mockResolvedValue({ id: 1, name: 'John' });
// Отклонение
mockFn.mockRejectedValue(new Error('API Error'));
// Разные результаты
mockFn
.mockResolvedValueOnce({ id: 1 })
.mockRejectedValueOnce(new Error('Error'))
.mockResolvedValue({ id: 2 });jest.mock() - Mock модуль
Полное мокирование модуля
typescript
// api.ts
export const fetchUser = async (id: number) => {
const res = await fetch(`/api/users/${id}`);
return res.json();
};
export const fetchPosts = async (userId: number) => {
const res = await fetch(`/api/users/${userId}/posts`);
return res.json();
};
// service.test.ts
jest.mock('./api');
import { fetchUser, fetchPosts } from './api';
test('uses mocked API', async () => {
(fetchUser as jest.Mock).mockResolvedValue({ id: 1, name: 'John' });
(fetchPosts as jest.Mock).mockResolvedValue([{ id: 1, title: 'Post 1' }]);
const user = await fetchUser(1);
const posts = await fetchPosts(1);
expect(user.name).toBe('John');
expect(posts[0].title).toBe('Post 1');
});Частичное мокирование (Manual Mock)
typescript
jest.mock('./api', () => ({
...jest.requireActual('./api'),
fetchUser: jest.fn()
}));jest.spyOn() - Spy (Шпионаж)
Шпионить за методом объекта
typescript
const obj = {
getValue: () => 42
};
const spy = jest.spyOn(obj, 'getValue');
const result = obj.getValue();
expect(spy).toHaveBeenCalled();
expect(result).toBe(42);
expect(spy).toHaveBeenCalledTimes(1);
spy.mockReturnValue(100);
expect(obj.getValue()).toBe(100);
spy.mockRestore(); // Восстановить оригинальную функциюШпионить за консолью
typescript
const consoleSpy = jest.spyOn(console, 'log');
console.log('Hello');
expect(consoleSpy).toHaveBeenCalledWith('Hello');
consoleSpy.mockRestore();jest.clearAllMocks() - Очистка
typescript
describe('Mock Clearing', () => {
const mockFn = jest.fn();
beforeEach(() => {
jest.clearAllMocks(); // Очистить все mocks
});
test('first test', () => {
mockFn('data');
expect(mockFn).toHaveBeenCalledTimes(1);
});
test('second test', () => {
// mockFn был очищен в beforeEach
expect(mockFn).not.toHaveBeenCalled();
mockFn('new data');
expect(mockFn).toHaveBeenCalledTimes(1);
});
});Практические примеры
Mock HTTP запроса
typescript
global.fetch = jest.fn();
beforeEach(() => {
(global.fetch as jest.Mock).mockClear();
});
test('fetches user', async () => {
(global.fetch as jest.Mock).mockResolvedValue({
json: async () => ({ id: 1, name: 'John' })
});
const response = await fetch('/api/user/1');
const user = await response.json();
expect(fetch).toHaveBeenCalledWith('/api/user/1');
expect(user.name).toBe('John');
});Mock localStorage
typescript
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn()
};
global.localStorage = localStorageMock as any;
test('saves token', () => {
const token = 'abc123';
localStorage.setItem('token', token);
expect(localStorage.setItem).toHaveBeenCalledWith('token', token);
});Mock Date
typescript
test('checks current date', () => {
const mockDate = new Date('2024-01-01');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any);
expect(new Date()).toEqual(mockDate);
});Mock класса
typescript
class Database {
connect() { /* ... */ }
query(sql: string) { /* ... */ }
}
const mockDb = {
connect: jest.fn(),
query: jest.fn().mockReturnValue([{ id: 1 }])
};
function getUserFromDb(db: Database, id: number) {
return db.query(`SELECT * FROM users WHERE id = ${id}`);
}
test('queries database', () => {
const result = getUserFromDb(mockDb as any, 1);
expect(mockDb.query).toHaveBeenCalled();
expect(result[0].id).toBe(1);
});Vitest Mocks
В Vitest используется vi вместо jest:
typescript
import { vi, test, expect } from 'vitest';
const mockFn = vi.fn();
vi.mock('./api');
const spy = vi.spyOn(obj, 'method');
vi.clearAllMocks();Лучшие практики
✅ Делайте:
- Mock только то, что нужно
- Восстанавливайте мокированные функции
- Проверяйте вызовы и аргументы
- Используйте descriptive имена для mock функций
- Очищайте моки между тестами
❌ Не делайте:
- ❌ Не мокируйте весь модуль если можно методы
- ❌ Не забывайте про mockRestore()
- ❌ Не создавайте слишком сложные моки
- ❌ Не тестируйте мок функции, тестируйте их использование
Дальше
Изучите Best Practices для полной картины.