Типизация тестового дубля так, чтобы он не разошёлся с подменяемым интерфейсом
Тест подменяет настоящий PaymentGateway дублем. Сейчас дубль — обычный объектный литерал, поэтому, когда интерфейс обзаводится методом или меняет сигнатуру, тест продолжает компилироваться против устаревшей формы и остаётся зелёным, пока прод ломается.
Требования: дубль должен проверяться по PaymentGateway, каждый член должен остаться шпионом, на вызовы которого можно писать проверки, а добавление метода в интерфейс обязано валить сборку теста, а не проходить молча.
interface PaymentGateway {
charge(cents: number, currency: 'usd' | 'eur'): Promise<{ id: string }>;
refund(id: string): Promise<void>;
}
const gateway = { charge: jest.fn(), refund: jest.fn() }; // your code here
await gateway.charge(500, 'usd');
Допишите реализацию.
Типизируйте дубль по контракту, а не рядом с ним: const gateway: jest.Mocked<PaymentGateway> = { charge: jest.fn(), refund: jest.fn() } — либо добавьте satisfies PaymentGateway. Тогда компилятор сверяет дубль с интерфейсом, и изменённая сигнатура валит сборку.
- ✗Приводить литерал через
as, что позволяет дублю молча разойтись с интерфейсом - ✗Хвататься за
as anyна дубле, что отвязывает его от подменяемой сущности - ✗Типизировать дубль как
Partial<T>, из-за чего недостающий метод никогда не ошибка
- →Когда дубль как
Partial<T>законен, и что для этого должно измениться в тестируемом коде? - →Как удержать фабрику фикстур в синхроне с типом, который она строит?
Решение
Два рабочих варианта — оба сверяют дубль с контрактом.
import { jest } from '@jest/globals';
interface PaymentGateway {
charge(cents: number, currency: 'usd' | 'eur'): Promise<{ id: string }>;
refund(id: string): Promise<void>;
}
// Вариант 1 — аннотация типом мока
const gateway: jest.Mocked<PaymentGateway> = {
charge: jest.fn(),
refund: jest.fn(),
};
// Вариант 2 — satisfies, сохраняющий точный тип каждого шпиона
const gateway2 = {
charge: jest.fn(),
refund: jest.fn(),
} satisfies PaymentGateway;
// интерфейс вырос:
// settle(id: string): Promise<void>;
// ❌ Property 'settle' is missing in type '{ charge: ...; refund: ... }'
// — сборка теста падает, устаревший дубль в прод не проедет
Почему это работает
Ключевая идея — дубль типизируется по контракту, а не рядом с ним. И аннотация jest.Mocked<PaymentGateway>, и satisfies PaymentGateway заставляют компилятор сравнить объект с интерфейсом: недостающий метод и разъехавшаяся сигнатура становятся ошибкой сборки.
Разница между вариантами — в том, что видно после: аннотация сводит члены к типу интерфейса, а satisfies оставляет точный выведенный тип каждого jest.fn(), что удобнее для mockResolvedValue.
⚠️ as PaymentGateway этого не даёт: приведение лишь просит компилятор поверить на слово, поэтому дубль спокойно расходится с интерфейсом. as any отвязывает его полностью.