JS Tower
Node.jsМодуль 7: Тестирование и деплой

Юнит-тестирование

Jest, Mocha, структура тестов

Цель урока

В этом уроке ты научишься:

  • Писать юнит-тесты с Jest
  • Структурировать тесты
  • Использовать моки и стабы

Jest

Установка

npm install jest --save-dev

package.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);
  });
});

Проверь себя

  1. Что такое matcher в Jest?
  2. Как мокнуть модуль?
  3. Зачем нужен beforeEach?