Привет!
В одной из недавних задач на Angular проекте, с которым я работаю, было обновление фреймворка для юнит тестов jest до 27.x версии. Если бы это был простой и безболезненный процесс, этой статьи не существовало бы. Так получилось, что в процессе обновления я наткнулся на все возможные ошибки. Поиск решения в интернете не дал толкового результата. Да, по итогу обновить получилось, но это было большое исследование и много времени поиска. И я решил сделать эту статью, чтобы другие люди, кто пытается обновить до jest 27.x версии не испытали таких проблем, которые были у меня.
TLDR;
Специально для тех, кому лень читать. Основная фишка этого обновления в настройках. Если же сомневаетесь и хотите посмотреть ошибки и способы их решения, то переходите к третьей части статьи.
План действий следующий:
Обновление пакетов
Самой важной частью обновления я считаю обновление всех пакетов, связанных с jest. Как я уже упоминал проект на Angular и недавно мы обновились до 12.x версии. И сразу после обновления появилась задача на обновление jest до версии 27.x.
В Angular вместе с jest нам нужно обновить следующие пакеты:
- jest – до latest версии (на момент написания статьи 27.0.6);
- jest-preset-angular – до latest версии (на момент написания статьи 9.0.5);
- @types/jest – также до latest версии (27.0.0).
Если нужные пакеты обновлены – идем дальше.
Обновляем настройки
Не менее важная часть обновления – настройки. Команда jest в своем блоге выпустила большую статью на тему обновления, но по моему мнению ей не хватает немного конкретики. Да, не скрою, я смог разобраться с обновлением, в том числе, благодаря этой статье, но прочитать ее пришлось несколько раз от начала до конца.
Важный момент по настройкам. Если вы обновляетесь с jest 15-26, то это для вас, если с более ранних версий, то вам придется пройти свой путь.
Первое, что нам нужно сделать – обновить transform.js файл
Трансформаторы были обновлены и теперь transform.js должен выглядеть следующим образом:
module.exports = require(“babel-jest”).default.createTransformer({
presets: [require.resolve(“@babel/preset-env”)]
});
Тут перед вызовом createTransformer у нас добавился вызов default.
Следующий шаг – обновление jest.config.js
Здесь нам важно обновить следующие опции – testEnvironment, moduleDirectories, transform. Поскольку именно эти настройки были изменены командой jest. Собственно изменения в файле jest.config.js:
transform: {
“^.+\\.(ts|html)$”: “jest-preset-angular”
},
testEnvironment: “jsdom”,
moduleDirectories: [
"node_modules",
"src"
],
Зеленым помечены нововведения версии jest 27.x и jest-preset-angular версии 9.x. После этих изменений большая часть ошибок уйдет, но все же в следующей секции я разберу ошибки более подробно
Ошибки и их решения
Первая ошибка, с которой я столкнулся, была TypeError: require(…).createTransformer is not a function.
Это оказалась самая простая ошибка – обновление файла transform.js чинит ее
Вторая ошибка — Component is not resolved
Эту, как и первую, ошибку я получал, запуская npm run test. Полный код ошибки выглядит так:
Component ‘ComponentName’ is not resolved:
- templateUrl: ./component-name.component.html
- styleUrls: [“./component-name.component.scss”]
Did you run and wait for ‘resolveComponentResources()’?
Судя по тому, что нам предлагается нужно резолвить компоненты каким-то образом. Одно из решений, найденных мною в гугле было добавить вызов TestBed.compileComponents(), но это был ложный след и вел он не туда.
По итогу, обновление jest.config.js решило проблему.
Третья ошибка — component is undefined
Это была одна из самых странных ошибок, которую гугл предлагал лечить таким же образом как и предыдущую, но по итогу после обновления jest.config.js все исчезло само собой. Код ошибки примерно следующий:
TypeError: Cannot read property ‘someMethod’ of undefined
test(‘should do something’ () => {
component.someMethod(‘foo’);
^
expect(result).toBeTruthy();
Ошибок такого типа, где компонент был почему-то не определен было примерно на половину тестов, и, как я уже упоминал, ошибка исчезла после обновления jest.config.js.
Ошибки, которые будут дальше, подразумевают, что у вас обновлены transform.js и jest.config.js
Четвертая — Expected done to be called once, but it was called multiple times
Тут все достаточно просто и понятно, в том числе и по документации jest:
The same
Один и тот же тестовый коллбэк done не может быть вызван более одного разаdonetest callback may not be called more than once
У меня эта ошибка воспроизводилась в основном в подписках на Observables, но все же было пару мест, где вызывалась в коде несколько раз. Решение простое – переносим вызов done дальше по коду или удаляем вызов done (для обычного кода):
Expected done to be called once, but it was called multiple times.
someService.methodWithObservable(‘bar’).subscribe((result) => {
expect(result).toEqual([]);
done();
^
});
Для Observables же слегка изменяем их структуру, добавляя операторы pipe и first (либо last), в случае, если по какой-либо причине нам нужны данные последней подписки:
someService.methodWithObservable(‘bar’)
.pipe().first().subscribe((result) => {
expect(result).toEqual([]);
done();
^
});
В случае вызова pipe и first(last) операторов, наш Observable будет выдавать только одну порцию результата, соответственно первую(first), либо последнюю(last).
Пятая — Test functions cannot both take a ‘done’ callback and return something
Также как и предыдущая ошибка эта ошибка была понятна после прочтения документации jest. В целом ее решение простое – либо убираем done() либо убираем return. У меня были случаи только с убиранием return, у вас может быть и то и то. Код ошибки также говорит сам за себя:
Test functions cannot both take a ‘done’ callback and return something. Either use a ‘done’ callback, or return a promise.
Шестая — Matcher error: received value must be a mock or spy function
В этом случае ошибка нам говорит не о многом, давайте посмотрим в то, что выдал терминал:
Expect(received).toHaveBeenCalledTimes(expected)
Matcher error: received value must be a mock or spy function
Received has type: function
Received has value: [Function setTimeout]
jest.useFakeTimers();
component.doSomething();
expect(setTimeout).toHaveBeenCalledTimes(1);
^
jest.runAllTimers();
Я заметил строчку jest.useFakeTimers(), что мне очень помогло. И тут я понял, что не зря прочитал несколько раз статью команды jest по обновлению:
modern fake timers implementation will now be the default. If you are among the unlucky few who are affected by the subtle implementation differences too heavily to migrate, you can get back the old implementation using
Современная имплементация фейковых таймеров теперь будет стандартным значением. Если вы среди тех, кому не повезло и миграция слишком тяжелая, вы можете вернуться к старой имплементации используяjest.useFakeTimers("legacy")or, if you are enabling fake timers globally via configuration,"timers": "legacy".jest.useFakeTimers("legacy")или, если вы включите фейковые таймеры глобально через конфигурацию,"timers": "legacy".
Собственно, добавить “legacy” к вызову useFakeTimers решило мою проблему. В вашем случае возможно нужно будет переписать код, но в целом это не должно стать большой проблемой.
Заключение
Как видим, если чуть больше систематизировать данные из статьи команды jest, то процесс обновления jest до версии 27.x не будет трудным и не займет слишком много времени. Если у вас возникли ошибки, которых нет в этой статье, пожалуйста, напишите в комментарии об этом, а также расскажите о вашем опыте обновления jest до версии 27.x. Надеюсь эта статья была полезна для вас.
Если вам стало интересно кто я, то просмотрите мой сайт-резюме.
