Тестирование и тесты типов
Тестирование типизированного кода — проверка типов в тестовых файлах, типобезопасные моки и фикстуры, альтернативы as any, проверка того, что вызов отвергается на этапе компиляции, через tsd или expect-type, и типизация кастомного матчера Jest.
6 вопросов
JuniorТеорияЧастоЧто проверяют tsd и expect-type, и почему обычный тест этого сделать не может?
Что проверяют tsd и expect-type, и почему обычный тест этого сделать не может?
Они утверждают факты о типах: что parse('x') — ровно string, а не any, что неверный аргумент отвергается, что обобщение выводит ожидаемое. Их вычисляет компилятор, поэтому они валят сборку. Тест в рантайме видит только значения: типы стёрты.
Типичные ошибки
- ✗Думать, что утверждение о типе падает во время прогона, а не при компиляции
- ✗Ожидать, что тест в рантайме увидит типы, которые стёрты до выполнения
- ✗Считать, что эти инструменты смотрят на реальные значения, а не на статические типы
Уточняющие вопросы
- →Как утверждать, что конкретный вызов — ошибка компиляции и остаётся ею?
- →Почему эти файлы обязаны попадать в проверку типов, чтобы утверждения вообще действовали?
JuniorТеорияЧастоЧто ловит проверка типов в тестовых файлах такого, чего не поймает их запуск?
Что ловит проверка типов в тестовых файлах такого, чего не поймает их запуск?
Компилятор проверяет, что тесты по-прежнему вызывают код правильно: переименованное поле или изменённая сигнатура валят сборку даже на строке, которую тест не выполняет. Запуск тестов проверяет поведение — и только на реально пройденных путях.
Типичные ошибки
- ✗Исключать тестовые файлы из проверки типов, из-за чего они молча расходятся с проверяемым кодом
- ✗Считать, что зелёный прогон означает, что тесты всё ещё вызывают API корректно
- ✗Ожидать, что компилятор проверит, что тест утверждает, а не то, как он вызывает
Уточняющие вопросы
- →Тест вызывает лишь одну перегрузку. Почему изменение другой сигнатуры всё равно валит сборку?
- →Что настроить, чтобы раннер и
tsc --noEmitвидели ровно один и тот же набор файлов?
MiddleКодЧастоТипизация тестового дубля так, чтобы он не разошёлся с подменяемым интерфейсом
Типизация тестового дубля так, чтобы он не разошёлся с подменяемым интерфейсом
Типизируйте дубль по контракту, а не рядом с ним: const gateway: jest.Mocked<PaymentGateway> = { charge: jest.fn(), refund: jest.fn() } — либо добавьте satisfies PaymentGateway. Тогда компилятор сверяет дубль с интерфейсом, и изменённая сигнатура валит сборку.
Типичные ошибки
- ✗Приводить литерал через
as, что позволяет дублю молча разойтись с интерфейсом - ✗Хвататься за
as anyна дубле, что отвязывает его от подменяемой сущности - ✗Типизировать дубль как
Partial<T>, из-за чего недостающий метод никогда не ошибка
Уточняющие вопросы
- →Когда дубль как
Partial<T>законен, и что для этого должно измениться в тестируемом коде? - →Как удержать фабрику фикстур в синхроне с типом, который она строит?
MiddleТеорияИногдаПочему as any в тестовом дубле — антипаттерн, и чем его заменить?
Почему as any в тестовом дубле — антипаттерн, и чем его заменить?
as any отвязывает дубль от того, что он дублирует: мок продолжает компилироваться после изменения настоящей сигнатуры, и набор остаётся зелёным, пока прод сломан. Сверяйте дубль с контрактом: satisfies T или jest.Mocked<T>.
Типичные ошибки
- ✗Считать тестовый код освобождённым от типобезопасности, раз он не уезжает в прод
- ✗Полагать, что устаревший дубль поймает прогон тестов, а не компилятор
- ✗Менять
as anyнаas unknown as T, что прячет ту же отвязку за двумя приведениями
Уточняющие вопросы
- →Вам нужны лишь два метода зависимости из двенадцати. Что изменить, чтобы частичный дубль стал законным?
- →Как заставить устаревший дубль валить CI, а не проходить тихо?
MiddleКодИногдаПроверка того, что неверный вызов отвергается при компиляции — и остаётся отвергнутым
Проверка того, что неверный вызов отвергается при компиляции — и остаётся отвергнутым
Поставьте @ts-expect-error над неверным вызовом. Он компилируется, только пока эта строка всё ещё падает, и сам становится ошибкой в день, когда вызов примут — поэтому утверждение не сгниёт, в отличие от вечно молчащего @ts-ignore. Файл обязан попадать в проверку типов.
Типичные ошибки
- ✗Пытаться проверить ошибку компиляции рантайм-проверкой
toThrow— код не компилируется, выполнять нечего - ✗Брать
@ts-ignore, который продолжает проходить после исчезновения ошибки и гниёт в мёртвую директиву - ✗Оставлять файл с тестами типов вне
tsconfig, который проверяет CI, из-за чего утверждение не вычисляется
Уточняющие вопросы
- →Почему
@ts-expect-errorвалит сборку, когда ошибка под ним исправлена? - →Как утверждать точный выведенный тип возврата, а не отвергнутый вызов?
MiddleКодИногдаТипизация кастомного матчера Jest, чтобы expect(x).toBeIsoDate() компилировался
Типизация кастомного матчера Jest, чтобы expect(x).toBeIsoDate() компилировался
expect.extend регистрирует только реализацию; на уровень типов из неё не попадает ничего. Метод добавляется слиянием объявлений в интерфейс матчеров Jest — declare global { namespace jest { interface Matchers<R> { toBeIsoDate(): R } } }. Слитый член и даёт сборку.
Типичные ошибки
- ✗Ожидать, что
expect.extendчто-то меняет на уровне типов — это лишь регистрация в рантайме - ✗Приводить тип в каждой точке вызова вместо одного слияния члена в интерфейс матчеров
- ✗Опустить
declare global, из-за чего дополнение остаётся локальным, и точка вызова всё ещё падает
Уточняющие вопросы
- →Как обобщённый параметр
RвMatchers<R>заставляет матчер работать с.notи с async? - →Где должен лежать файл объявлений, чтобы дополнение подхватилось проверкой типов?