Создание custom matchers

В Jasmine существует мощный механизм для расширения стандартного набора матчеров с помощью custom matchers. Это позволяет создавать свои методы проверки, которые делают тесты более читаемыми и выразительными, а также позволяют сократить дублирование кода при сложных проверках.

Основы custom matcher

Custom matcher — это функция, которая возвращает объект с ключом compare. В этом объекте описывается логика проверки значения и возвращается результат в виде объекта с полями:

  • pass — булево значение, показывающее, прошла ли проверка;
  • message — строка с пояснением, которая выводится при неудачном тесте.

Простейший пример:

beforeEach(function() {
  jasmine.addMatchers({
    toBeEven: function() {
      return {
        compare: function(actual) {
          const result = {};
          result.pass = actual % 2 === 0;
          result.message = result.pass
            ? `Ожидалось, что ${actual} не будет чётным`
            : `Ожидалось, что ${actual} будет чётным`;
          return result;
        }
      };
    }
  });
});

describe('Пример custom matcher', function() {
  it('проверяет чётность числа', function() {
    expect(4).toBeEven();
    expect(5).not.toBeEven();
  });
});

Здесь ключевое:

  • jasmine.addMatchers регистрирует matcher глобально для текущего набора тестов;
  • Функция compare получает фактическое значение (actual) и, при необходимости, дополнительные аргументы;
  • Возврат объекта с pass и message позволяет Jasmine корректно отобразить результат.

Использование аргументов

Custom matcher может принимать дополнительные параметры. Например, проверка, что число находится в диапазоне:

beforeEach(function() {
  jasmine.addMatchers({
    toBeWithinRange: function() {
      return {
        compare: function(actual, floor, ceiling) {
          const result = {};
          result.pass = actual >= floor && actual <= ceiling;
          result.message = result.pass
            ? `Ожидалось, что ${actual} не находится в диапазоне от ${floor} до ${ceiling}`
            : `Ожидалось, что ${actual} находится в диапазоне от ${floor} до ${ceiling}`;
          return result;
        }
      };
    }
  });
});

describe('Проверка числа в диапазоне', function() {
  it('число находится между 5 и 10', function() {
    expect(7).toBeWithinRange(5, 10);
    expect(12).not.toBeWithinRange(5, 10);
  });
});

Аргументы после actual передаются в matcher через expect(value).matcher(arg1, arg2).

Асинхронные проверки

Jasmine поддерживает работу с промисами и асинхронными функциями. Custom matcher можно интегрировать с async/await, если внутренняя логика зависит от асинхронного вызова. Важно возвращать результат синхронно, а асинхронную работу обрабатывать заранее:

beforeEach(function() {
  jasmine.addMatchers({
    toResolveWithValue: function() {
      return {
        compare: async function(actualPromise, expected) {
          const result = {};
          try {
            const value = await actualPromise;
            result.pass = value === expected;
            result.message = result.pass
              ? `Ожидалось, что промис не вернёт ${expected}`
              : `Ожидалось, что промис вернёт ${expected}, но вернул ${value}`;
          } catch (err) {
            result.pass = false;
            result.message = `Промис завершился с ошибкой: ${err}`;
          }
          return result;
        }
      };
    }
  });
});

describe('Асинхронный custom matcher', function() {
  it('проверяет результат промиса', async function() {
    const promise = Promise.resolve(42);
    await expectAsync(promise).toResolveWithValue(42);
  });
});

Для асинхронных матчеров используется expectAsync, и matcher должен корректно обрабатывать промис.

Повторное использование и модульность

Custom matchers можно организовать в отдельные модули для повторного использования. Например, создать файл matchers.js:

export const numericMatchers = {
  toBeEven: function() { /* реализация */ },
  toBeWithinRange: function() { /* реализация */ }
};

И подключать в тестах:

import { numericMatchers } from './matchers';

beforeEach(function() {
  jasmine.addMatchers(numericMatchers);
});

Это улучшает читаемость кода и поддерживаемость крупных проектов.

Советы по написанию custom matcher

  1. Ясные сообщения — каждый matcher должен выдавать понятное объяснение при ошибке.
  2. Единообразие API — параметры должны быть интуитивно понятны.
  3. Тестируемость matcher — matcher сам по себе может быть протестирован отдельными unit-тестами.
  4. Не злоупотреблять сложными вычислениями внутри matcher, лучше проверять простые условия и использовать вспомогательные функции.
  5. Использовать not — логика должна корректно работать как при положительном, так и при отрицательном вызове.

Встроенные методы для кастомизации

Jasmine позволяет использовать дополнительные поля в объекте compare:

  • negativeCompare — можно отдельно определить поведение для expect(...).not.toMatcher(). В большинстве случаев достаточно писать только compare, так как Jasmine автоматически инвертирует pass.
  • customMessage — можно формировать более сложные динамические сообщения, используя значения аргументов и фактического результата.

Создание кастомных матчеров — один из ключевых инструментов для повышения выразительности тестов в Jasmine. Они помогают сделать проверки более читаемыми, модульными и адаптированными к специфике проекта.