Типизация пользовательских матчеров

Jest предоставляет мощные возможности для тестирования, включая поддержку пользовательских матчеров. Такие матчеры позволяют расширить функциональность стандартных методов проверки, добавив специфические проверки, которые могут быть полезны для конкретных нужд проекта. Однако, чтобы эффективно использовать их с TypeScript, важно правильно типизировать эти матчеры, обеспечив их корректную работу и поддержку автодополнения.

Создание пользовательского матчера

Для начала рассмотрим, как создать простой пользовательский матч. Допустим, нам нужно написать матч для проверки, что строка содержит определённое подстрочное значение.

expect.extend({
  toContainSubstring(received, argument) {
    const pass = received.includes(argument);
    if (pass) {
      return {
        message: () => `expected ${received} not to contain ${argument}`,
        pass: true,
      };
    } else {
      return {
        message: () => `expected ${received} to contain ${argument}`,
        pass: false,
      };
    }
  },
});

Этот матч проверяет, что строка received содержит подстроку argument. Однако если в проекте используется TypeScript, нам нужно дополнительно типизировать этот матчер, чтобы гарантировать правильную работу с типами.

Типизация пользовательских матчеров с TypeScript

Когда создаётся пользовательский матч, необходимо описать типы для передаваемых аргументов и возвращаемого значения. Jest уже включает типы для стандартных матчеров, но для пользовательских матчеров их необходимо задать вручную.

1. Описание типа для функции матчера

Каждый матч в Jest — это функция, которая принимает два аргумента: фактическое значение и ожидаемое значение. В случае пользовательского матчера важно правильно типизировать эти параметры. Для этого можно использовать типы, которые предоставляет Jest, такие как MatcherContext и Matchers из пакета @jest/globals.

Пример типизации матчера:

import { MatcherContext, Matchers } from '@jest/globals';

declare global {
  namespace jest {
    interface Matchers<R> {
      toContainSubstring(expected: string): R;
    }
  }
}

expect.extend({
  toContainSubstring(received: string, argument: string) {
    const pass = received.includes(argument);
    if (pass) {
      return {
        message: () => `expected ${received} not to contain ${argument}`,
        pass: true,
      };
    } else {
      return {
        message: () => `expected ${received} to contain ${argument}`,
        pass: false,
      };
    }
  },
});

Здесь мы добавили интерфейс Matchers, расширив его на метод toContainSubstring. Это позволяет TypeScript корректно распознавать наш матчер в тестах.

2. Типы возвращаемых значений

Функция матчера должна возвращать объект, который содержит два ключевых поля: pass (булевое значение, которое указывает, прошёл ли тест) и message (функция, которая возвращает сообщение об ошибке). Важно правильно типизировать этот объект.

interface MatcherResult {
  pass: boolean;
  message: () => string;
}

В функции нашего матчера этот результат будет возвращаться через объект:

toContainSubstring(received: string, argument: string): MatcherResult {
  const pass = received.includes(argument);
  if (pass) {
    return {
      message: () => `expected ${received} not to contain ${argument}`,
      pass: true,
    };
  } else {
    return {
      message: () => `expected ${received} to contain ${argument}`,
      pass: false,
    };
  }
}

Такой подход гарантирует, что TypeScript будет точно знать, какой тип возвращаемого значения ожидать.

Расширение стандартных матчеров

В Jest можно расширить не только функциональность, но и типизацию стандартных матчеров. Например, добавление новых аргументов в матчеры или создание комбинированных матчеров.

Пример комбинированного матчера, который проверяет и тип, и содержимое строки:

declare global {
  namespace jest {
    interface Matchers<R> {
      toBeStringAndContainSubstring(expected: string): R;
    }
  }
}

expect.extend({
  toBeStringAndContainSubstring(received: unknown, argument: string) {
    if (typeof received !== 'string') {
      return {
        message: () => `expected a string, but received ${typeof received}`,
        pass: false,
      };
    }
    const pass = received.includes(argument);
    if (pass) {
      return {
        message: () => `expected ${received} not to contain ${argument}`,
        pass: true,
      };
    } else {
      return {
        message: () => `expected ${received} to contain ${argument}`,
        pass: false,
      };
    }
  },
});

В данном примере мы проверяем тип received, а затем проверяем, содержит ли он подстроку. Важно, чтобы в случае ошибки тип был корректно указан, а также правильно сформулировано сообщение об ошибке.

Использование пользовательских матчеров в тестах

После того как пользовательский матч был написан и типизирован, его можно использовать в тестах, как обычный матч. Пример использования:

test('string contains substring', () => {
  expect('Hello, world!').toContainSubstring('world');
});

test('checks string type and content', () => {
  expect('Hello, world!').toBeStringAndContainSubstring('world');
});

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

Советы по типизации

  • Типы входных данных: всегда уточняйте типы входных данных, чтобы избежать ошибок при использовании матчеров с неподходящими типами.
  • Использование интерфейсов: для более сложных матчеров, которые работают с объектами или массивами, полезно определять интерфейсы или типы для аргументов.
  • Типы возвращаемых объектов: возвращаемое значение матчеров всегда должно соответствовать интерфейсу с полями pass и message. Это помогает избежать неожиданных ошибок и гарантировать корректную работу тестов.

Заключение

Правильная типизация пользовательских матчеров в Jest не только улучшает поддержку автодополнения, но и повышает надёжность тестов. Использование TypeScript с Jest позволяет создавать сложные, но типобезопасные расширения для проверки специфичных условий, что делает код тестов более чётким и предсказуемым.