Перейти к содержимому

ESLint-плагин ​

Плагин находит тестовый код, который проходит, ничего не проверяя, и рукописные подмены, которые отстают от заменяемого класса. Он входит в vitest-auto-spy, так что ставить больше ничего не нужно. Добавьте его в eslint.config.js для своих спек-файлов:

js
// eslint.config.js
import autoSpy from 'vitest-auto-spy/eslint-plugin';

export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];

Плагину нужен flat config (eslint.config.js). Старый формат .eslintrc загрузить его не может: он ищет пакет с именем eslint-plugin-*, а этот плагин — subpath другого пакета.

У каждого правила есть свой раздел в Правилах ESLint: о чём оно сообщает, пример «до и после», опции и когда его выключить. Эта страница — про подключение и настройку.

Как подключить его к проекту ​

1. Блок конфигурации ​

Раскройте (оператор ..., spread) один из готовых конфигов в блоке для спек-файлов. Во flat config более поздний блок перекрывает более ранний, поэтому ставьте этот блок после остальных конфигов:

js
// eslint.config.js
import tseslint from 'typescript-eslint';
import autoSpy from 'vitest-auto-spy/eslint-plugin';

export default [
  ...tseslint.configs.recommended,
  {
    files: ['**/*.spec.ts', '**/*.test.ts'],
    ...autoSpy.configs.recommended,
  },
];

Каждый конфиг — обычный объект с двумя ключами, plugins и rules. Когда вы раскрываете его в свой блок, приходят оба ключа.

КонфигЧто включаетКогда брать
configs.recommendedвсе 58 правил: 45 на error, 13 на warnвы начинаете работать с плагином; это вариант по умолчанию
configs.strictвсе 58 правил на errorваши спеки чисты, и любая находка должна останавливать сборку
configs.typeErrorsprefer-as-spy и no-mocked-for-spy на errorвместе с recommended, в рецепте для большого проекта

Оба правила из typeErrors уже стоят на error в recommended. typeErrors нужен только в рецепте, который понижает все правила до warn, а эти два возвращает обратно; см. раздел Как внедрить на большом существующем наборе тестов без красного CI.

Некоторые правила ни о чём не сообщают, пока не узнают, как ваш проект собирается и запускает тесты:

  • no-compile-components ждёт опцию builder.
  • no-redundant-mock-reset ждёт свои опции или конфиг Vitest, который сможет найти и прочитать.
  • no-relative-mock-under-builder ждёт свою опцию или таргет unit-test-билдера Angular в angular.json, project.json или nx.json, который запускает файл.

2. Глоб files не опционален ​

Глоба по умолчанию у плагина нет: где лежат ваши спеки, знаете только вы. Неверный глоб ломается одним из двух способов:

  • Слишком узкий: плагин ничего не проверяет и ни о чём не сообщает. Это самая частая проблема настройки. Проверьте, что применяется на самом деле, одной командой.
  • Слишком широкий: правила начинают срабатывать на прикладном коде. Например, Object.defineProperty в прикладном коде уместен, а no-object-define-property о нём сообщит.

Типичные глобы:

js
files: ['**/*.spec.ts'],                                  // Angular
files: ['**/*.test.ts', '**/*.test.tsx'],                 // React / Vue / Node
files: ['**/*.{spec,test}.{ts,tsx}', '**/test/**/*.ts'],  // и то и другое плюс папка с тестами

В монорепозитории один блок в корне покрывает все пакеты, если глоб начинается с **/: '**/*.spec.ts' совпадает и с packages/*/src/**. Второй блок добавляйте только для пакета, чьим спекам нужны другие уровни серьёзности.

3. Информация о типах нужна только четырём правилам ​

Пятьдесят три правила читают только сам файл. Они работают без parserOptions.project, заметно не замедляют линт и работают, даже если ваших спек нет ни в одном tsconfig.

Четыре правила читают типы TypeScript: no-private-member-access, no-mistyped-use-value, no-unknown-use-value-key и prefer-to-have-signal-value. Без информации о типах они ни о чём не сообщают. Единственное исключение — проверка Object.getPrototypeOf в no-private-member-access: она работает в любом случае.

Чтобы включить эти четыре, передайте парсеру проект, где ваши спеки уже входят в tsconfig:

js
languageOptions: {
  parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
},

4. Как выглядит первый прогон ​

На существующем проекте первый прогон, скорее всего, будет красным. Сорок пять правил стоят на error, и так задумано.

Тринадцать правил стоят на warn. Они попадают в вывод, но не валят сборку:

Правило, которое читает один файл, не может знать о проекте всё, поэтому три правила могут сработать на корректном коде. Если находка выглядит неверной, загляните в раздел Три правила, которые могут сработать на корректном коде.

Первый заход укорачивают две команды:

bash
npx eslint . --fix                         # правила с автоисправлением сами переписывают код
npx eslint . --format stylish | tail -30   # в сводке видно, какое правило срабатывает чаще всего

Всё, что осталось, — либо настоящая находка, либо правило, которое вам пока не нужно. Про оба случая — раздел Настройка под ваш проект. Если первый прогон слишком большой, чтобы исправить всё сразу, следуйте разделу Как внедрить на большом существующем наборе тестов без красного CI.

Какие правила касаются вас ​

Четыре правила — про Jasmine. Если вы никогда не пользовались Jasmine, на вашем коде они сработать не могут, так что их можно оставить включёнными.

ВыЧто плагин делает для вас
пишете на Vitest, никогда не трогали Jasmine или Jestработают 52 основных правила; четыре правила про Jasmine никогда не срабатывают
переезжаете с jest-auto-spies / Jestработу делают основные правила; больше всего ловят no-done-callback и prefer-as-spy
переезжаете с jasmine-auto-spiesвсе 58 правил; поставьте prefer-native-spy-api в 'off', пока не уберёте слой совместимости с Jasmine

Если вы никогда не пользовались Jasmine ​

Каждое правило про Jasmine ищет код, которого в Vitest-тестах нет:

ПравилоЧто ищетВ ваших спеках
no-jasmine-globalsjasmine.* и голые глобалы spyOn(, spyOnProperty(, fail(, pending(вы пишете vi.spyOn и expect.fail; голые формы под Vitest бросают ReferenceError
jasmine-namespace-without-entry.and / .calls / .withArgs на спае, созданном этой библиотекойвы пишете .mockReturnValue, .mock.calls и calledWith
no-save-arguments-by-valuespy.calls.saveArgumentsByValue()API есть только в Jasmine; код на Vitest его никогда не вызывает
prefer-native-spy-apiте же пространства имён .and / .callsто же, что выше

jasmine-namespace-without-entry не смотрит ни на что под .mock, поэтому spy.mock.calls[0] оно не отмечает никогда.

Выключить эти четыре можно, но это ничего не экономит: правило, которому не на что срабатывать, лишней работы не делает. Если всё же хочется конфиг покороче:

js
export default [
  {
    files: ['**/*.spec.ts'],
    ...autoSpy.configs.recommended,
    rules: {
      ...autoSpy.configs.recommended.rules,
      // мы никогда не пользовались jasmine; этим четырём тут нечего сказать
      'vitest-auto-spy/no-jasmine-globals': 'off',
      'vitest-auto-spy/jasmine-namespace-without-entry': 'off',
      'vitest-auto-spy/no-save-arguments-by-value': 'off',
      'vitest-auto-spy/prefer-native-spy-api': 'off',
    },
  },
];

Если вы приходите из Jest ​

Отдельного набора правил для Jest нет. Большую часть того, что нужно поменять в Jest-тестах, ловят эти основные правила:

ПравилоЧто ловит в Jest-тестах
no-done-callbackit('x', (done) => …). Vitest передаёт туда объект контекста, так что done() бросает. Если вызов стоит внутри колбэка, тест проходит, не выполнившись
prefer-as-spyTestBed.inject(X) as Spy<X>. С этой библиотекой это падает с TS2352, и это самая частая ошибка компиляции после переезда Angular-проекта
no-mocked-for-spyобъявления Mocked<T>, которые требуют приватных полей, которых у спая нет
no-shared-module-level-mockэкспортированную фикстуру { save: jest.fn() }. При isolate: false (файлы тестов в одном воркере делят модули) это один объект на воркер, а не на тест
prefer-create-spy-from-classобъект из fn(), используемый как подмена. Он отстаёт от класса, как только у класса появляется новый метод
no-floating-assertionexpect() в .then(), которого никто не дожидается

no-jasmine-globals оставьте включённым и здесь. До Jest 27 Jest работал на jest-jasmine2, а тот ставил spyOn, fail и pending глобалами, так что в старых Jest-тестах они могут встретиться. Опасен spyOn: версия Jasmine заменяет метод, а vi.spyOn вызывает настоящий метод. После простого переименования спека компилируется и выполняется, а проверяемый код обращается к настоящему коллаборатору.

Для массовой правки запустите кодмод, а не линтер: npx vitest-auto-spy codemod --from jest. --from jasmine обрабатывает другой диалект, а auto по умолчанию выбирает нужный набор для каждого файла. См. Переезд с jest-auto-spies.

Если вы приходите из Jasmine ​

Касаются все 58 правил, а четыре правила про Jasmine написаны прямо для вас:

  • no-jasmine-globals и no-save-arguments-by-value сообщают о поведении, которое молча меняется после переименования.
  • jasmine-namespace-without-entry сообщает о спае, созданном до установки слоя совместимости.
  • prefer-native-spy-api сообщает о самом слое совместимости. На время переезда выключите его:
js
'vitest-auto-spy/prefer-native-spy-api': 'off', // удалите это на последней миле

Для массовой правки запустите npx vitest-auto-spy codemod --from jasmine. Он делает и те правки, которые правило не исправляет автоматически. См. Переезд с jasmine-auto-spies.

Настройка под ваш проект ​

Каждый конфиг — обычный объект, так что любая настройка ниже — одна строка в вашемeslint.config.js.

Как внедрить на большом существующем наборе тестов без красного CI ​

На большом существующем проекте первый прогон может выдать сотни находок. Этот рецепт держит CI зелёным, пока вы их исправляете, и всё равно валит сборку на находках, которые не компилируются.

  1. Один раз запустите npx eslint . --fix. Он исправит большую часть находок двух правил про ошибки типов (prefer-as-spy и no-mocked-for-spy), которые шаг 2 оставляет на error. Остальные находки этих правил исправьте руками: редактор предложит каждую правку как подсказку. --fix применяет автоисправления всех правил, у которых они есть, а не только этих двух, поэтому просмотрите дифф перед коммитом.
  2. Понизьте все правила до warn, а правила про ошибки типов верните на error через configs.typeErrors:
js
// eslint.config.js
import tseslint from 'typescript-eslint';
import autoSpy from 'vitest-auto-spy/eslint-plugin';

const asWarnings = Object.fromEntries(Object.keys(autoSpy.configs.recommended.rules).map((rule) => [rule, 'warn']));

export default [
  ...tseslint.configs.recommended,
  {
    files: ['**/*.spec.ts'],
    ...autoSpy.configs.recommended,
    rules: {
      ...autoSpy.configs.recommended.rules,
      ...asWarnings,
      ...autoSpy.configs.typeErrors.rules, // находки, которые не компилируются, остаются ошибками
      'vitest-auto-spy/prefer-create-spy-from-class': 'off', // свои изменения — после всех `...`
    },
  },
];
  1. Исправляйте предупреждения партиями. Когда их не останется, удалите asWarnings и строку с typeErrors.

Предупреждения не валят eslint, если не запускать его с --max-warnings. Если ваш CI передаёт --max-warnings 0, уберите этот флаг на время рецепта. Свои изменения правил пишите после последнего ..., как в примере, иначе раскрытые конфиги их перезапишут.

Прежде чем понижать всё, посмотрите, что на самом деле в первом прогоне:

  • Двенадцать правил на warn и так дают предупреждения. На проекте с компонентами prefer-render-shallow часто самое громкое правило, так что проверьте, какую часть первого прогона даёт оно.
  • no-compile-components, no-redundant-mock-reset и no-relative-mock-under-builder молчат, пока опция (а для двух последних ещё ваш конфиг или таргет билдера) не скажет им, как запускается ваш проект.
  • Остальное — в основном правила о том, что тест неправильный. Красный CI нужен именно для них.

Вариант, который надёжнее на долгий срок, когда падающие файлы умещаются в список, который можно вести руками: оставьте каждое правило на его уровне по умолчанию и выключите падающие правила только для ещё не исправленных файлов. Сокращающийся список путей показывает прогресс. Общий warn на весь проект обычно остаётся навсегда.

js
export default [
  { files: ['**/*.spec.ts'], ...autoSpy.configs.recommended },
  {
    files: ['src/app/legacy/**/*.spec.ts', 'src/app/cart/cart.component.spec.ts'], // ещё не исправлены
    rules: { 'vitest-auto-spy/prefer-provide-auto-spy': 'off', 'vitest-auto-spy/no-done-callback': 'off' },
  },
];

Почему на error остаются именно эти два. Это не «самые важные правила» — такой выбор за вами. Это правила, чьи находки не компилируются:

  • TestBed.inject(X) as Spy<X> падает с TS2352.
  • let s: Mocked<T> падает с TS2322.

Все остальные правила сообщают о коде, который компилируется и работает, поэтому исправлять его партиями — реальный план. Для этих двух — нет: сборка уже красная. В перенесённом с jest-auto-spies файле часто по десять таких приведений, по одному на каждую внедрённую подмену. Под общим warn он проходит линтер, а потом падает на проверке типов с десятью ошибками, которые упоминают accessorSpies (свойство, которое добавляет Spy<T>) и ни разу не называют правило. У обоих правил есть --fix, так что держать их на error стоит один запуск eslint --fix.

Сейчас в этот набор входят prefer-as-spy и no-mocked-for-spy — два правила из таблицы Типы. Раскрывайте конфиг, а не копируйте два имени: список поддерживает пакет, а копия в вашем конфиге молча устареет.

Понизить одно правило ​

js
export default [
  {
    files: ['**/*.spec.ts'],
    ...autoSpy.configs.recommended,
    rules: {
      ...autoSpy.configs.recommended.rules,
      'vitest-auto-spy/prefer-provide-auto-spy': 'warn', // сообщать, но не блокировать мердж
      'vitest-auto-spy/prefer-create-spy-from-class': 'off', // не наш стиль
    },
  },
];

Сначала раскройте autoSpy.configs.recommended.rules, потом пишите свои переопределения: в объекте побеждает более поздний ключ. ...autoSpy.configs.recommended уже приносит ключ rules, и ключ rules, написанный после него, заменяет весь этот объект правил. Без внутреннего раскрытия в конфиге останутся только ваши два правила, и никакая ошибка об этом не скажет.

Чтобы поднять правило с warn до error, напишите такую же строку: 'vitest-auto-spy/prefer-render-shallow': 'error'.

Три правила, которые могут сработать на корректном коде ​

Эти три правила отвечают на вопрос, на который один файл не всегда может ответить. Они всё равно стоят на error: если правило ошибается насчёт вашего проекта, это исправляется одной строкой. Если первый прогон вас удивил, смотрите сначала на них.

ПравилоКогда оно ошибается насчёт васСтрока, которая это чинит
jasmine-namespace-without-entryenableJasmineCompat() выполняется в файле из setupFiles Vitest, который не импортирует ни одна спека, поэтому файл с .and выглядит так, будто слоя нет['error', { setupModules: ['./test-setup'] }]
no-unregistered-inject-spyфайл регистрирует часть подмен так, как правило умеет читать, и получает ещё одну через хелпер, за которым оно не следит, например общий beforeEachопции нет; локальный 'off' или отключение на строку
prefer-native-spy-apiвы посреди переезда с jasmine-auto-spies; оно сообщает о рабочем коде слоя совместимости на каждой строке'off', пока тесты не позеленеют, потом 'error' и --fix на последнюю милю

setupModules говорит jasmine-namespace-without-entry, где ставится слой, и правило перестаёт гадать. Импорт vitest-auto-spy/bun, …/node или …/rstest тоже глушит правило для этого файла. Эти точки входа не могут загрузить vitest-auto-spy/jasmine, поэтому на их рантаймах слой всегда ставится из setup-файла.

no-unregistered-inject-spy редко что-то нужно: оно и так осторожно. Оно молчит, пока файл хотя бы раз не вызовет provideAutoSpy. Ещё оно умолкает, когда встречает то, что не может прочитать: спред или неизвестную фабрику провайдера в providers, createWithAutoSpies, renderShallow или TestBed.overrideProvider. То, что остаётся, обычно настоящая находка: injectSpy(X) возвращает настоящий сервис, и его спай-хелперы бросают на первом же .mockReturnValue(…).

Заглушить одну строку, а не одно правило ​

ts
// eslint-disable-next-line vitest-auto-spy/no-object-define-property -- свойство это геттер на замороженном host-объекте
Object.defineProperty(target, 'clientWidth', { value: 100 });

Если исключение локальное, это лучше, чем выключать правило через off. Правило продолжает работать во всех остальных файлах, а комментарий записывает, чем эта строка отличается.

Проверить, что применяется на самом деле ​

bash
npx eslint --print-config src/app/cart.spec.ts | grep vitest-auto-spy

Если ничего не напечаталось, глоб files не совпадает с этим файлом. Выглядит это так же, как «плагин не нашёл проблем», поэтому проверьте это, прежде чем решить, что спеки чистые.

Рядом с vitest/expect-expect ​

Они не пересекаются. expect-expect сообщает о тесте, в котором ассерта нет. Этот плагин сообщает об ассерте, который есть, но ничего не проверяет. Включайте оба.

Настраивайте assertFunctionNames шаблонами, а не списком имён:

js
'vitest/expect-expect': ['error', { assertFunctionNames: ['expect*', 'assert*', '**.expect*'] }],
  • expect* покрывает expectEmission, expectEmissions, expectNoEmission, expectCompletion и expectError из этого пакета, а также ваши собственные хелперы expect….
  • assert* покрывает assertNoPendingRequests, assertNoShadowedProviders и assertMocked.
  • **.expect* покрывает хелпер, который вызывают через объект.

Список имён приходится дополнять на каждый новый хелпер. Он может и сломаться: в одних тестах хелпер-ассерт назывался find, и find в списке принимает за ассерт любой Array.prototype.find. Шаблоны выше дали ноль ложных срабатываний на 1759 спек-файлах.

Рядом с vitest/require-hook ​

require-hook сообщает о любом вызове в теле describe или в начале спеки, который не является хуком. Оно не может знать, что useConsoleSpies() или setupFakeTimers() сами регистрируют хуки. Плагин экспортирует список таких хелперов: каждую публичную функцию этого пакета, которая вызывает beforeEach, afterEach, beforeAll или afterAll. Тест в этом пакете проверяет список, так что новый хелпер не может из него выпасть.

js
import autoSpy from 'vitest-auto-spy/eslint-plugin';

'vitest/require-hook': ['error', { allowedFunctionCalls: [...autoSpy.hookRegisteringHelpers] }],

Вызовов, которые не регистрируют хуков, например registerSignalMatchers() и trackStrayTimers(), в списке нет. Если вы вызываете их в начале линтуемого файла, допишите их после спреда.

Опции правил ​

Опции есть у десяти правил. Полная таблица опций — в разделе каждого правила в Правилах ESLint.

ПравилоОпцииЧто делают
prefer-create-spy-from-classminRunnerFns (по умолчанию 2)сколько vi.fn() делают объект подменой
no-stub-class-doubleminRunnerFns (по умолчанию 1)сколько полей vi.fn() делают класс подменой
no-structural-doubleminRunnerFns (по умолчанию 2)сообщает только ниже этого числа; держите равным значению у prefer-create-spy-from-class или поднимите (скажем, до 100), если то правило выключено
prefer-render-shallowtemplates: 'as-needed' (по умолчанию) или 'never''never' запрещает настоящие шаблоны в юнит-спеках
prefer-inject-spyignoreTokensтокены, чей внедрённый экземпляр остаётся настоящим
no-real-component-providerignoreTokens, childInjectors (по умолчанию false)токены, которые пропускаются; читать и дочерние инжекторы
no-redundant-mock-resetclearMocks, mockReset, restoreMocks, configFile, configFlagsчто раннер сбрасывает между тестами
no-compile-componentsbuilder: 'inline-resources', ignoreComponentsсообщает, что билдер встраивает шаблоны; без неё правило молчит
no-relative-mock-under-builderbuilder: 'unit-test'сообщает, что эти спеки запускает unit-test-билдер Angular, когда правило не может найти таргет само
jasmine-namespace-without-entrysetupModulessetup-файлы, которые ставят слой совместимости с Jasmine

Форма-массив задаёт и уровень серьёзности. ['error', { templates: 'never' }] поднимает prefer-render-shallow с warn, его уровня в recommended, до error. Чтобы оставить warn, пишите ['warn', { … }].

js
'vitest-auto-spy/prefer-render-shallow': ['error', { templates: 'never' }],
'vitest-auto-spy/no-compile-components': ['error', { builder: 'inline-resources' }],
'vitest-auto-spy/no-redundant-mock-reset': ['error', { clearMocks: true, restoreMocks: true }],

Выбирать правила руками ​

Можно не брать configs.recommended и перечислить только нужные правила. Объект плагина экспортируется отдельно, так что это поддерживаемая настройка. Если нужен весь набор с парой изменений, см. Настройка под ваш проект. Если нужен короткий список — пишите так:

js
import autoSpy from 'vitest-auto-spy/eslint-plugin';

export default [
  {
    files: ['**/*.spec.ts'],
    plugins: { 'vitest-auto-spy': autoSpy },
    rules: {
      'vitest-auto-spy/no-expect-in-subscribe': 'error',
      'vitest-auto-spy/no-done-callback': 'error',
    },
  },
];

Что содержит сообщение ​

Каждое сообщение:

  1. называет, что правило нашло в этом файле: класс, токен, член или вызов;
  2. одним предложением говорит, почему это ломается;
  3. даёт одно исправление;
  4. заканчивается на Docs: и ссылкой на раздел правила в Правилах ESLint.

Эта же ссылка — meta.docs.url правила, поэтому редактор делает имя правила ссылкой на неё. Правила входят в пакет вместе с API, который они советуют, так что всегда совпадают с установленной у вас версией.

Правила ​

В таблицах ниже у каждого правила одна строка. Имя правила ведёт на его полный раздел в Правилах ESLint.

  • По умолчанию: уровень серьёзности в configs.recommended. Чтобы его изменить, см. Понизить одно правило.
  • Правка: --fix значит, что eslint --fix переписывает код. подсказка значит, что редактор предлагает правку, а вы принимаете её вручную. Почему так поделено, объясняет раздел Какие правила чинят и почему их так мало.
  • Без него: что покажет прогон тестов, если правило выключено. зелено — плохой случай: сломанный тест всё равно проходит, и больше ничто вам об этом не скажет. красно значит, что прогон падает, но с менее полезным сообщением. компиляция значит, что падает проверка типов. — значит, что правило сообщает о мёртвом коде или о цене, а не об отказе. (по построению) значит, что значение следует из того, как работают раннер и код, и не проверялось пробным прогоном.

Ассерты, которые никогда не выполняются ​

Тест проходит, потому что ассерт не выполнился или выполнился, но упасть не мог. Например, поток промолчал или промис никто не дождался.

ПравилоНа что срабатываетПо умолчаниюПравкаБез него
no-expect-in-subscribeexpect() внутри колбэка subscribe() → expectEmission / firstValueFromerrorподсказказелено
no-vacuous-absence-assertionтест, каждый ассерт которого выполняется и тогда, когда поток ничего не отправил → expectNoEmission / expectEmissionerror—зелено
no-floating-assertionexpect() в .then(), которого никто не дожидается → expect(await promise)error—зелено
no-done-callbackit('x', (done) => …) и done.fail(…) → async-тест с дождавшимся ассертомerror—зелено
no-bare-called-withspy.m.calledWith(1); отдельной строкой: недописанная заглушка, которая ничего не проверяетerror—зелено
no-constant-expectexpect(true).toBe(true): значение, записанное в спеке, предрешает ответ матчераerror—зелено
no-redundant-smoke-testit('should create', () => expect(pipe).toBeTruthy()) рядом с тестами, которые уже создают субъект → удалитьerrorподсказказелено
prefer-settle-dynamic-importawait import('./thing') в теле теста → await settleDynamicImport(() => import('./thing'))errorподсказказелено
no-real-wait-in-testawait new Promise((r) => setTimeout(r, 300)): настоящий сон → advanceTimers(300) или vi.waitFor(…)warn—зелено
no-inline-test-dataфикстура на 20 строк или один объект три раза в спеке → экспорт из файла *.mock.tswarn——
no-self-called-spyтест сам вызывает метод под спаем, а потом проверяет, что его вызвали → вызовите настоящий триггерerror—зелено
no-unasserted-argumentголый toHaveBeenCalled() там, где по файлу видно, что важны аргументы → toHaveBeenCalledWith(…)warn——

Подмены и модули, которые их держат ​

Эти правила — про то, что один файл оставляет следующему.

ПравилоНа что срабатываетПо умолчаниюПравкаБез него
prefer-create-spy-from-classобъект из двух и более vi.fn() → createSpyFromClass / createAutoMockerror—красно
no-stub-class-doubleкласс, чьи поля — vi.fn() → createSpyFromClass / provideAutoSpywarn—красно
no-structural-doubleобъект из vi.fn() с объявленным типом { load: Mock } → createAutoMock<T>()warn—красно
prefer-spy-on-own-methodcreateSpyFromInstance на один метод → spyOnOwnMethod(x, 'm') / spyOnVoidMethod(x, 'm')warn--fix / подсказказелено
no-shared-module-level-mockэкспортированное значение, в котором лежат vi.fn() → экспортируйте фабрику, которая его возвращаетerror—зелено
no-outer-binding-in-mock-factoryфабрика vi.mock, которая читает const / let / class верхнего уровня → объявите его через vi.hoistederror—красно
no-object-define-propertyObject.defineProperty в спеке → mockReadonlyProp / mockValueProperrorподсказказелено
no-import-time-spreadexport const x = [...Imported] на уровне модуля: TypeError или пустой объект при загрузке бандлаerrorподсказкакрасно (по построению)
prefer-observer-stubрукописный глобал IntersectionObserver / ResizeObserver / MutationObserver → stubIntersectionObserver() и соседиerror—зелено
no-hand-assigned-globalglobal.fetch = vi.fn(…) без восстановления → mockValueProp / vi.stubGlobal / blockNetwork()error--fix (импортированный объект)зелено
prefer-stub-responseобъект, приведённый к Response, или createMock<Response>(…) → stubResponse({ body })error—зелено
no-redundant-mock-resetсброс в хуке, который раннер и так делает между тестами → удалите; молчит, пока не знает флагов раннераerror--fix / подсказка—

Angular DI и TestBed ​

Эти правила ловят провайдер или спай на компоненте, который оказался не тем, что спека, по её мнению, зарегистрировала.

ПравилоНа что срабатываетПо умолчаниюПравкаБез него
prefer-provide-auto-spyрукописная подмена в useValue / useFactory / useClass / useExisting → provideAutoSpy(Class) / provideAutoSpyForToken(TOKEN)error--fixкрасно
prefer-provide-activated-routeсобранный руками ActivatedRoute или provideAutoSpy(ActivatedRoute) → provideActivatedRoute({ … })error—красно
prefer-inject-spyvi.spyOn(TestBed.inject(X), 'm') → injectSpy(X).merrorподсказкакрасно
no-unregistered-inject-spyinjectSpy(X) для токена, который этот файл не регистрировал: вы получаете настоящий экземплярerror—красно (по построению)
no-real-component-providerпровайдер уровня компонента, прочитанный из фикстуры, когда его никто не подменил → overrideComponentProvider(Component, X)error——
prefer-render-shallowTestBed.createComponent в файле, который ни разу не читает шаблон → renderShallow(X)warnподсказказелено
prefer-set-inputsfixture.componentRef.setInput('title', v) → await setInputs(fixture, { title: v })warnподсказказелено
prefer-to-have-signal-valueexpect(component.total()).toBe(3) → expect(component.total).toHaveSignalValue(3); нужны типыwarn--fixкрасно
no-overridden-providerдва провайдера на один токен или провайдер, который заменяет TestBed.overrideProvider: более ранний не выполняетсяerrorподсказказелено
no-inject-before-overrideTestBed.inject() / injectSpy() / renderShallow() в хуке, в тестах, которые ещё вызывают override*error—красно
no-dead-schemasschemas на тестовом модуле, который ничего не объявляет: схеме не к чему применятьсяerror—зелено (по построению)
no-mistyped-use-valueuseValue, который не подходит под примитивный тип, объявленный его токеном; нужны типыerror—зелено (по построению)
no-unknown-use-value-keyключ в объектном useValue, которого нет у предоставляемого типа; нужны типыerror—зелено (по построению)
no-instance-lifecycle-spyvi.spyOn(component, 'ngOnInit'): спай хука на экземпляре Angular никогда не вызываетwarn—зелено (заглушка)
no-compile-componentscompileComponents() под билдером, который встраивает шаблоны; молчит, пока нет { builder: 'inline-resources' }errorподсказка— (мёртвая строка)
no-relative-mock-under-buildervi.mock('./x') в спеке, которую запускает @angular/build:unit-test → provideAutoSpy(X) / overrideComponentProvidererror—красно
no-disabled-testbed-teardownteardown: { destroyAfterEach: false } → удалите; каждая фикстура переживает свой тестerror—зелено
no-sync-testbed-awaitawait на configureTestingModule / override* / createComponent, которые не возвращают промисerrorподсказка— (мёртвый await)

Доступ мимо публичного API ​

Эти правила сообщают о тестах, которые читают приватные члены проверяемого класса.

ПравилоНа что срабатываетПо умолчаниюПравкаБез него
no-private-member-accessinstance['privateMember'], (instance as any).privateMember, vi.spyOn(Object.getPrototypeOf(x), 'm'); нужны типыerror—зелено (по построению)
no-reflect-member-accessReflect.get(component, 'x') / Reflect.set(service, 'x', v): то же самое, но с ключом, который не проверяет компиляторerrorподсказказелено (по построению)

Типы ​

Два правила сообщают о касте или типе, который компилятор отвергает с запутанным сообщением. Три сообщают о коде, который выключает компилятор: каст над фикстурой, каст над методом спая и комментарий над заглушкой.

ПравилоНа что срабатываетПо умолчаниюПравкаБез него
no-mocked-for-spyMocked<T> в позиции типа → Spy<T>, вместе с импортомerror--fix / подсказкакомпиляция
prefer-as-spyTestBed.inject(X) as Spy<X> → asSpy(TestBed.inject(X)), вместе с импортомerror--fixкомпиляция (по построению)
prefer-create-mockобъектный литерал под as SomeType → createMock<SomeType>({ … })warnподсказка—
no-mock-castTestBed.inject(S).m as Mock → injectSpy(S).merrorподсказка—
no-ts-expect-error-on-double@ts-expect-error / @ts-ignore над nextWith, mockReturnValue, calledWith(…) спаяerror—зелено

Консоль ​

Эти правила сообщают о выводе в консоль, который не увидит ни один консольный спай. Это пара к setupAutoSpy({ strayConsole }) на этапе линта: та роняет тест во время прогона.

ПравилоНа что срабатываетПо умолчаниюПравкаБез него
no-passthrough-console-spyvi.spyOn(console, 'error') без реализации: он всё равно печатает → installConsoleSpies()errorподсказказелено (по построению)
no-console-in-specспека, которая вызывает console.x(…) или присваивает console.x = … и не возвращает его обратноerror—зелено (по построению)
no-import-time-console-spiesимпорт vitest-auto-spy/console в файле, который ни разу не вызывает installConsoleSpies() или useConsoleSpies()error—зелено (по построению)
no-unasserted-console-spyконсольный спай, который файл никогда не проверяет → проверьте его или дайте useConsoleSpies() заглушить консольwarn—зелено

Уход с jasmine ​

Эти правила — для тестов, которые ещё работают на vitest-auto-spy/jasmine или ещё содержат код на Jasmine. В тестах, которые никогда не пользовались Jasmine, они не срабатывают; см. Если вы никогда не пользовались Jasmine.

ПравилоНа что срабатываетПо умолчаниюПравкаБез него
no-jasmine-globalsjasmine.*, голые spyOn( / spyOnProperty( / spyOnAllFunctions( / fail( / pending(, .withContext(error—зелено (случай spyOn)
jasmine-namespace-without-entry.and / .calls / .withArgs на спае библиотеки в файле, который ни разу не ставит слой совместимостиerror—красно (по построению)
no-save-arguments-by-valuespy.calls.saveArgumentsByValue(): здесь он ничего не делает, и спека проверяет изменившееся состояниеerror—зелено (по построению)
prefer-native-spy-api.and / .calls там, где собственный API спая делает то же самоеerror--fix / подсказка— (сообщает о рабочем коде)

Какие правила чинят и почему их так мало ​

Девять из 58 правил переписывают код под --fix. Девятнадцать предлагают правку как подсказку в редакторе, которую вы принимаете вручную: у четырёх из них для некоторых форм есть и --fix (в таблице ниже), а пятнадцать предлагают только подсказку. Граница зависит от того, чего стоит ошибочная догадка, а не от того, насколько сложна правка.

Правило--fixПодсказка
prefer-as-spyвсегда—
no-mocked-for-spyпараметр, возвращаемый тип или каст; переменная, чьё значение приходит из фабрик этой библиотекиостальные переменные
prefer-provide-auto-spyда—
prefer-to-have-signal-valueда—
no-hand-assigned-globalзначение, записанное в импортированный объект—
no-redundant-mock-resetсброс, который раннер доказуемо уже сделалостальные случаи
prefer-spy-on-own-methodточные формыостальное
prefer-native-spy-apiкогда спай доказуемо создан этой библиотекойво всех остальных местах

Пятнадцать правил предлагают только подсказку: no-expect-in-subscribe, prefer-inject-spy, no-object-define-property, no-overridden-provider, no-reflect-member-access, no-import-time-spread, prefer-render-shallow, prefer-settle-dynamic-import, prefer-create-mock, no-mock-cast, no-passthrough-console-spy, no-compile-components, no-sync-testbed-await, no-redundant-smoke-test и prefer-set-inputs.

Исправление работает без присмотра, поэтому ошибиться ему должно быть безопасно. Правило получает --fix, когда ошибочная правка падает громко или невозможна:

  • no-mocked-for-spy и prefer-as-spy меняют только типы. Ошибочная правка не даст файлу скомпилироваться, а это самое громкое и дешёвое падение из возможных.
  • prefer-as-spy сохраняет ваш собственный каст: asSpy — типизированная функция-тождество, так что новая строка утверждает ровно то же, что старая.

Подсказка — для правки, которая меняет поведение. Вы видите дифф и принимаете её по одному вызову:

  • Найдёт ли injectSpy(X) спай, зависит от provideAutoSpy(X), который обычно лежит в другом файле.
  • mockValueProp оставляет свойство записываемым там, где Object.defineProperty его запечатал.
  • no-expect-in-subscribe переписывает тест целиком.
  • no-overridden-provider удаляет строку провайдера; no-import-time-spread превращает константу в функцию, и к каждому использованию нужно дописать ().

Никакого исправления там, где ремонт затрагивает весь файл, а не один узел. createSpyFromClass нужен класс, который объектный литерал нигде не называет. provideAutoSpy теряет возвращаемые значения, которые настроил useValue. Object.defineProperties превращается в отдельный mockValueProp на каждую запись.

В разделе каждого правила в Правилах ESLint точно сказано, когда оно предлагает исправление или подсказку и какие формы не трогает.

Измерено: чего стоит каждое правило ​

Колонка Без него в разделе Правила взята из настоящих прогонов. Для каждого правила написали одну пробную спеку с ассертом, который не может быть истинным. Пробы гоняли на Vitest 4.1.9, с isolate: false там, где это важно, а половину про zone — под zone.js.

ПравилоБез правила прогон говоритВердикт
no-expect-in-subscribeничего: 4 из 4 форм зелёные на 4 поведениях потоказелено
no-done-callbackничего, если done() стоит в колбэке: тело возвращает undefined, тест заканчивается, ассерт выполняется после негозелено
no-floating-assertionбез zone: Unhandled Rejection, код выхода 1, тест не назван. Под zone.js: одно из двух отклонений пропадаетзелено
no-bare-called-withничего: спай, как и раньше, отвечает undefined на эти аргументы, а тест не проверяет ни одного вызовазелено
no-shared-module-level-mockничего: собственное состояние фикстуры переходит между файлами при isolate: falseзелено
no-object-define-propertyничего в файле, который подменил свойство; следующий файл читает подменённое значениезелено
no-mocked-for-spyTS2322 … missing the following properties from type 'CartService': http, cacheкомпиляция
prefer-create-spy-from-classTypeError: cart.applyCoupon is not a functionкрасно
prefer-provide-auto-spyто же самое, на один шаг DI дальшекрасно
prefer-inject-spyspy.getPlans.nextWith is not a functionкрасно
no-inject-before-overrideCannot override provider when the test module has already been instantiated. Make sure you are not using \inject` before `overrideProvider``красно
no-overridden-providerничего, если рукописная подмена случайно отвечает как надо. Если прочитать через injectSpy, прогон красный, и injectSpy говорит почемузелено

Семь из этих тринадцати защищают от теста, который зелёный и неправильный. Сам прогон о таком отказе сообщить не может. Четыре защищают от красного теста, чьё сообщение и так понятно, и одно — от ошибки компиляции.

Эта колонка — доказательство, а не уровень серьёзности. Конфиг не выводит из неё серьёзность: насколько громкой должна быть находка, решает ваш проект. Тринадцать правил на warn выбраны по другим признакам, перечисленным в разделе Как выглядит первый прогон.

Четыре правила про Jasmine так не проверяли: их предмет — миграция, а не поведение раннера. Два из них зелёные по построению: saveArgumentsByValue() здесь ничего не делает, а то, что vi.spyOn вызывает настоящий метод, — документированное поведение Vitest.

no-overridden-provider — единственное правило, чей вердикт зависит от остального файла. С TestBed.inject и ассертами на рукописную подмену всё проходит, а provideAutoSpy над ней так и не выполнился.

Во что плагин обходится на прогоне ​

Весь конфиг recommended почти не добавляет времени линту: 56 мс на 173 спек-файла и 68 мс на одну спеку размером 1,7 МБ. Больше замеров — на странице Производительность.

Опубликовано под лицензией MIT.