Matchers (Проверки)

Matchers — функции сравнения в Jest, которые проверяют, что результат теста соответствует ожиданию.

Зачем нужно

Matchers — язык утверждений в тестах. toBe, toEqual, toThrow — каждый matcher проверяет свой аспект. Правильный выбор matcher делает тест читаемым и даёт понятные сообщения об ошибках.

Где используется

В каждом тесте Jest/Vitest. Строка expect(value).matcher — основа любого теста.

Предпосылки

Настройка Jest, Основы тестирования

Базовые matchers

toBe — строгое равенство (===)

test('toBe — примитивы', () => {
  expect(2 + 2).toBe(4);
  expect('hello').toBe('hello');
  expect(true).toBe(true);
  expect(null).toBe(null);
  expect(undefined).toBe(undefined);
});

// ОСТОРОЖНО: для объектов toBe проверяет ссылку!
test('toBe НЕ работает для объектов', () => {
  const a = { x: 1 };
  const b = { x: 1 };
  expect(a).not.toBe(b);    // Разные ссылки!
  expect(a).toBe(a);         // Одна ссылка — ОК
});

toEqual — глубокое сравнение

test('toEqual — объекты и массивы', () => {
  expect({ a: 1, b: 2 }).toEqual({ a: 1, b: 2 });
  expect([1, 2, 3]).toEqual([1, 2, 3]);

  // Вложенные объекты
  expect({
    user: { name: 'Алиса', scores: [10, 20] },
  }).toEqual({
    user: { name: 'Алиса', scores: [10, 20] },
  });
});

// toStrictEqual — строже: проверяет undefined свойства и тип
test('toStrictEqual vs toEqual', () => {
  expect({ a: 1, b: undefined }).toEqual({ a: 1 });       // PASS
  expect({ a: 1, b: undefined }).not.toStrictEqual({ a: 1 }); // разные!
});

Truthiness — проверки на «правдивость»

test('truthiness matchers', () => {
  expect(null).toBeNull();
  expect(undefined).toBeUndefined();
  expect('hello').toBeDefined();

  expect(true).toBeTruthy();
  expect(1).toBeTruthy();
  expect('text').toBeTruthy();

  expect(false).toBeFalsy();
  expect(0).toBeFalsy();
  expect('').toBeFalsy();
  expect(null).toBeFalsy();
  expect(undefined).toBeFalsy();
});

Числа

test('числовые matchers', () => {
  expect(10).toBeGreaterThan(5);
  expect(10).toBeGreaterThanOrEqual(10);
  expect(5).toBeLessThan(10);
  expect(5).toBeLessThanOrEqual(5);

  // Для дробных чисел — НЕ используй toBe!
  expect(0.1 + 0.2).not.toBe(0.3);              // Ошибка IEEE 754
  expect(0.1 + 0.2).toBeCloseTo(0.3, 5);         // OK, 5 знаков

  // NaN
  expect(NaN).toBeNaN();

  // Infinity
  expect(1 / 0).toBe(Infinity);
});

Строки

test('строковые matchers', () => {
  expect('Hello World').toContain('World');
  expect('Hello World').toMatch(/hello/i);      // RegExp
  expect('user@email.com').toMatch(
    /^[\w.-]+@[\w.-]+\.\w{2,}$/
  );

  // Длина
  expect('abc').toHaveLength(3);
});

Массивы и итерируемые объекты

test('массивы', () => {
  const fruits = ['яблоко', 'банан', 'вишня'];

  expect(fruits).toContain('банан');
  expect(fruits).toHaveLength(3);
  expect(fruits).toEqual(expect.arrayContaining(['вишня', 'яблоко']));

  // Не содержит
  expect(fruits).not.toContain('груша');

  // Содержит объект
  const users = [
    { id: 1, name: 'Алиса' },
    { id: 2, name: 'Боб' },
  ];
  expect(users).toContainEqual({ id: 1, name: 'Алиса' });
});

Объекты

test('объекты', () => {
  const user = {
    id: 1,
    name: 'Алиса',
    email: 'alice@test.com',
    address: { city: 'Москва' },
  };

  // Проверка свойства
  expect(user).toHaveProperty('name');
  expect(user).toHaveProperty('name', 'Алиса');
  expect(user).toHaveProperty('address.city', 'Москва');

  // Частичное совпадение
  expect(user).toMatchObject({
    name: 'Алиса',
    email: 'alice@test.com',
  });

  // expect.objectContaining — внутри других matchers
  expect(user).toEqual(expect.objectContaining({
    name: expect.any(String),
    id: expect.any(Number),
  }));
});

Исключения

test('исключения', () => {
  function divide(a, b) {
    if (b === 0) throw new Error('Деление на ноль');
    return a / b;
  }

  // Обязательно оборачиваем в функцию!
  expect( => divide(10, 0)).toThrow;
  expect( => divide(10, 0)).toThrow('Деление на ноль');
  expect( => divide(10, 0)).toThrow(/ноль/);
  expect( => divide(10, 0)).toThrow(Error);

  // Не выбрасывает
  expect( => divide(10, 2)).not.toThrow;
});

Функции-моки

test('проверка вызовов мок-функций', () => {
  const callback = jest.fn;

  callback('a');
  callback('b', 'c');
  callback('a');

  // Был ли вызван
  expect(callback).toHaveBeenCalled();
  expect(callback).toHaveBeenCalledTimes(3);

  // С какими аргументами
  expect(callback).toHaveBeenCalledWith('a');
  expect(callback).toHaveBeenCalledWith('b', 'c');

  // Последний вызов
  expect(callback).toHaveBeenLastCalledWith('a');

  // N-й вызов (0-indexed)
  expect(callback).toHaveBeenNthCalledWith(1, 'a');
  expect(callback).toHaveBeenNthCalledWith(2, 'b', 'c');

  // Вернул значение (если mockReturnValue задан)
  const fn = jest.fn.mockReturnValue(42);
  fn;
  expect(fn).toHaveReturnedWith(42);
});

Snapshot matchers

test('snapshot', () => {
  const user = createUser('Алиса', 25);

  // Полный снапшот (сохраняется в .snap файл)
  expect(user).toMatchSnapshot;

  // Inline snapshot (встраивается прямо в тест)
  expect(user).toMatchInlineSnapshot(`
    {
      "age": 25,
      "id": Any<Number>,
      "name": "Алиса",
    }
  `);
});

Модификатор .not

test('.not инвертирует проверку', () => {
  expect(5).not.toBe(3);
  expect('hello').not.toContain('xyz');
  expect([1, 2]).not.toHaveLength(3);
  expect({ a: 1 }).not.toHaveProperty('b');
  expect(() => {}).not.toThrow;
});

Асимметричные matchers (expect.xxx)

test('expect.any и expect.anything', () => {
  expect({ id: 42, created: new Date }).toEqual({
    id: expect.any(Number),
    created: expect.any(Date),
  });

  // anything — что угодно кроме null/undefined
  expect({ key: 'value' }).toEqual({
    key: expect.anything,
  });

  // stringContaining
  expect({ msg: 'Ошибка: файл не найден' }).toEqual({
    msg: expect.stringContaining('файл не найден'),
  });

  // stringMatching — с RegExp
  expect({ email: 'test@mail.com' }).toEqual({
    email: expect.stringMatching(/@/),
  });
});

Кастомные matchers

// Расширяем expect своими проверками
expect.extend({
  toBeWithinRange(received, floor, ceiling) {
    const pass = received >= floor && received <= ceiling;
    if (pass) {
      return {
        message:  => `ожидалось, что ${received} НЕ будет в диапазоне ${floor}-${ceiling}`,
        pass: true,
      };
    } else {
      return {
        message:  => `ожидалось, что ${received} будет в диапазоне ${floor}-${ceiling}`,
        pass: false,
      };
    }
  },
});

test('кастомный matcher', () => {
  expect(50).toBeWithinRange(1, 100);
  expect(150).not.toBeWithinRange(1, 100);
});

Шпаргалка: выбор matcher

Что проверяю Matcher
Примитив (число, строка, boolean) toBe
Объект / массив toEqual / toStrictEqual
Дробное число toBeCloseTo
Содержит элемент toContain / toContainEqual
Строка по паттерну toMatch
Свойство объекта toHaveProperty
Выбрасывает ошибку toThrow
null / undefined toBeNull / toBeUndefined
Мок вызван toHaveBeenCalledWith
Снапшот toMatchSnapshot

Частые ошибки

1. toBe вместо toEqual для объектов

// ПЛОХО:
expect({ a: 1 }).toBe({ a: 1 }); // FAIL — разные ссылки

// ХОРОШО:
expect({ a: 1 }).toEqual({ a: 1 }); // PASS

2. toThrow без оборачивающей функции

// ПЛОХО: ошибка выбросится ДО expect
expect(divide(1, 0)).toThrow; // TypeError!

// ХОРОШО: оборачиваем в стрелочную функцию
expect( => divide(1, 0)).toThrow;

3. Сравнение дробных чисел через toBe

// ПЛОХО:
expect(0.1 + 0.2).toBe(0.3); // FAIL (0.30000000000000004)

// ХОРОШО:
expect(0.1 + 0.2).toBeCloseTo(0.3);

Практика

  1. Напиши тесты для функции findMax(arr): пустой массив, один элемент, отрицательные числа, дубликаты
  2. Протестируй функцию parseUrl(str): проверь каждое свойство через toHaveProperty
  3. Создай кастомный matcher toBeValidEmail и используй его в тестах
  4. Используй expect.objectContaining для проверки частичного совпадения API-ответа

Связанные темы

Ресурсы