Node.jsМодуль 7: Тестирование и деплой
Юнит-тестирование
Jest, Mocha, структура тестов
Цель урока
В этом уроке ты научишься:
- Писать юнит-тесты с Jest
- Структурировать тесты
- Использовать моки и стабы
Jest
Установка
npm install jest --save-devpackage.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}jest.config.js
module.exports = {
testEnvironment: 'node',
coverageDirectory: 'coverage',
collectCoverageFrom: ['src/**/*.js'],
testMatch: ['**/*.test.js'],
verbose: true
};Структура теста
// math.js
function add(a, b) {
return a + b;
}
function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
module.exports = { add, divide };// math.test.js
const { add, divide } = require('./math');
describe('Math functions', () => {
describe('add', () => {
test('adds two positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('adds negative numbers', () => {
expect(add(-1, -2)).toBe(-3);
});
});
describe('divide', () => {
test('divides two numbers', () => {
expect(divide(10, 2)).toBe(5);
});
test('throws on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
});Matchers
// Равенство
expect(value).toBe(5); // ===
expect(value).toEqual({ a: 1 }); // Глубокое сравнение
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
// Числа
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3);
expect(value).toBeLessThan(5);
expect(value).toBeCloseTo(0.3, 5); // Для float
// Строки
expect(string).toMatch(/pattern/);
expect(string).toContain('substring');
// Массивы
expect(array).toContain(item);
expect(array).toHaveLength(3);
// Объекты
expect(obj).toHaveProperty('key');
expect(obj).toMatchObject({ key: 'value' });
// Исключения
expect(() => fn()).toThrow();
expect(() => fn()).toThrow('error message');Асинхронные тесты
// async/await
test('fetches user', async () => {
const user = await getUser(1);
expect(user.name).toBe('Иван');
});
// Promises
test('fetches user', () => {
return getUser(1).then(user => {
expect(user.name).toBe('Иван');
});
});
// Callbacks
test('fetches user', (done) => {
getUser(1, (err, user) => {
expect(user.name).toBe('Иван');
done();
});
});Setup и Teardown
describe('Database tests', () => {
beforeAll(async () => {
await connectDB();
});
afterAll(async () => {
await disconnectDB();
});
beforeEach(async () => {
await clearDB();
});
afterEach(() => {
jest.clearAllMocks();
});
test('...', () => {});
});Моки
Мок функции
const mockFn = jest.fn();
mockFn('arg1', 'arg2');
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenCalledTimes(1);Мок возвращаемого значения
const mockFn = jest.fn()
.mockReturnValue(10)
.mockReturnValueOnce(5);
console.log(mockFn()); // 5 (первый вызов)
console.log(mockFn()); // 10 (остальные)
// Async
const asyncMock = jest.fn().mockResolvedValue({ data: 'value' });Мок модуля
// Мок всего модуля
jest.mock('./database');
const db = require('./database');
db.findUser.mockResolvedValue({ id: 1, name: 'Test' });
// Частичный мок
jest.mock('./utils', () => ({
...jest.requireActual('./utils'),
fetchData: jest.fn()
}));Тестирование сервисов
// userService.js
class UserService {
constructor(userRepository) {
this.userRepository = userRepository;
}
async createUser(data) {
const exists = await this.userRepository.findByEmail(data.email);
if (exists) throw new Error('Email exists');
return this.userRepository.create(data);
}
}
// userService.test.js
describe('UserService', () => {
let userService;
let mockRepository;
beforeEach(() => {
mockRepository = {
findByEmail: jest.fn(),
create: jest.fn()
};
userService = new UserService(mockRepository);
});
test('creates user when email is unique', async () => {
mockRepository.findByEmail.mockResolvedValue(null);
mockRepository.create.mockResolvedValue({ id: 1, email: 'test@test.com' });
const user = await userService.createUser({ email: 'test@test.com' });
expect(user.id).toBe(1);
expect(mockRepository.create).toHaveBeenCalled();
});
test('throws when email exists', async () => {
mockRepository.findByEmail.mockResolvedValue({ id: 1 });
await expect(
userService.createUser({ email: 'test@test.com' })
).rejects.toThrow('Email exists');
});
});Практика
Задание: Тест валидатора
Задача: Напиши тесты для функции валидации email.
Решение:
// validator.js
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// validator.test.js
describe('isValidEmail', () => {
test('returns true for valid email', () => {
expect(isValidEmail('test@example.com')).toBe(true);
});
test('returns false for invalid email', () => {
expect(isValidEmail('invalid')).toBe(false);
expect(isValidEmail('test@')).toBe(false);
expect(isValidEmail('@example.com')).toBe(false);
});
});Проверь себя
- Что такое matcher в Jest?
- Как мокнуть модуль?
- Зачем нужен beforeEach?