Разница между очисткой, сбросом и восстановлением моков
- среда, 30 октября 2024 г. в 00:00:06
Одним из источников путаницы вокруг моков, является то, что моки могут быть с отслеживанием состояния.
Например, эта функция:
const fn = vi.fn()
fn('one')
fn('two')
fn.mock.calls
// [ ["one"], ["two"] ]
В приведенном выше примере функция-заглушка fn имеет состояние, которое отслеживает все вызовы, сделанные к ней. Само состояние не является проблемой. На самом деле, вам это состояние нужно! Оно необходимо для проверки правильного количества вызовов и их аргументов во время теста.
Когда оно изменяется или сохраняется, когда вы этого не ожидаете. Чрезвычайно распространенным примером неправильно управляемого состояния является следующее:
const listener = vi.fn()
test('calls the listener when the event is emitted', () => {
const emitter = new Emitter()
emitter.on('hello', listener)
emitter.emit('hello')
expect(listener).toHaveBeenCalledTimes(1)
})
test('does not call the listener after `.removeAllListeners()` has been called', () => {
const emitter = new Emitter()
emitter.on('hello', listener)
emitter.removeAllListeners()
emitter.emit('hello')
expect(listener).not.toHaveBeenCalled()
})
Здесь нам нужно протестировать случаи для класса Emitter
. Первый из них ожидает, что слушатель будет вызван при эмитировании события, а другой ожидает отсутствия вызовов слушателя, потому что все слушатели были удалены.
Чтобы это проверить, мы вводим мок функцию listener
, чтобы проверять вызовы.
Но если мы запустим этот набор тестов, мы получим ошибку 😱
FAIL src/example.test.ts > does not call the listener after `.removeAllListeners()` has been calle
AssertionError: expected "spy" to not be called at all, but actually been called 1 times
Received:
1st spy call:
Array []
Number of calls: 1
❯ src/example.test.ts:19:24
17| emitter.emit('hello')
18|
19| expect(listener).not.toHaveBeenCalled()
| ^
20| })
21|
Хотя наши тестовые случаи имеют собственную область, объявляя и управляя своими ресурсами, функция listener
становится общим состоянием между ними. В результате вызов listener
из первого теста просачивается во второй, где вызовов не ожидается.
Есть несколько способов решить эту проблему. Мы можем объявить мок функцию listener
в отдельных тестах, чтобы предотвратить её совместное использование, или мы можем управлять состоянием этой мок функции.
Давайте поговорим о втором подходе.
И Vitest, и Jest предоставляют отличные API для управления состоянием ваших моков:
.mockClear() / vi.clearAllMocks()
.mockReset() / vi.resetAllMocks()
.mockRestore() / vi.restoreAllMocks()
Вы можете применять эти методы как к отдельным мок-функциям, так и ко всему набору тестов.
Есть только одна вещь.
Названия этих методов очень похожи 😅
Когда я задал этот вопрос в Твиттере, большинство людей не смогли уверенно отличить их друг от друга. Честно говоря, когда я перечитал документацию, чтобы убедиться, что сам всё правильно понял, у меня началась небольшая паника от мысли, что я использовал .resetAllMocks() вместо .restoreAllMocks() в своём воркшопе (всё в порядке, прошлый я использовал их правильно).
Пришло время развеять путаницу раз и навсегда.
Мы подробно рассмотрим отдельные методы .mock*, потому что их аналоги vi.*AllMocks делают то же самое, просто применяют к каждому существующему моку одновременно.
Во-первых, важно признать, что любая мок функция отслеживает две вещи:
Список вызовов (т.е. как эта функция вызывается);
Реализация функции (т.е. что делает эта функция при вызове).
Все методы управления состоянием, о которых мы будем говорить ниже, направлены на то, чтобы помочь вам сбросить одно или оба из этих состояний.
Представьте вашу мок функцию как коробку. Каждый вызов этой функции добавляет в коробку красивый синий шарик. Вы можете проверить, сколько шариков находится в коробке в любой момент времени, но может наступить момент, когда вам потребуется полностью опустошить эту коробку.
Это то, что делает метод .mockClear()
.Он очищает все записанные вызовы мок функции, и на этом всё.
const fn = vi.fn()
fn('one')
expect(fn).toHaveBeenCalledTimes(1) // ✅
// Заставляет мок функцию забыть о
// любых вызовах, произошедших до этого момента..
fn.mockClear()
fn('two')
expect(fn).toHaveBeenCalledTimes(1) // ✅
Метод .mockClear()
является хорошим выбором для очистки записанной информации о вызовах внутри одного теста. Это особенно полезно при тестировании сложных, часто повторяющихся сценариев, включающих ту же мок функции.
fn('one')
// ...assert
fn.mockClear()
fn('two')
// ...assert
Продолжая нашу аналогию с коробкой, давайте представим, что помимо того, чтобы класть мячи в нашу коробку, мы можем также поместить внутрь неё более маленькую коробку (реализацию мока). Если мы это сделаем, то эта маленькая коробка будет получать все мячи и распоряжаться ими по своему усмотрению.
Сброс мока означает как очистку его вызовов, так и удаление любой реализации мока, которая могла быть установлена. Это опустошает коробку и выбрасывает внутреннюю маленькую коробку, если она есть.
Метод .mockReset()
чрезвычайно удобен, когда вы имеете дело с функциями, имеющими моковую реализацию. И это может включать как мок-функции, так и "шпионы"!
// Давайте подслушаем консоль!
// Вызов vi.spyOn оборачивает глобальную функцию console.log
// в мок-функцию. Давайте также предоставим мок-реализацию
// чтобы вызовы консоли в тесте ничего не выводили.
const spy = vi.spyOn(console, 'log').mockImplementation(() => {})
console.log('you will not see this') // ничего не выводится!
expect(console.log).toHaveBeenCalledTimes(1) // ✅
// Заставляет мок забыть о любых вызовах к нему
// и также удаляет любую мок-реализацию, которая у него могла быть.
spy.mockReset()
console.log('you will see this!') // выводит "you will see this"
expect(console.log).toHaveBeenCalledTimes(1) // ✅
Тот факт, что сам мок остается активным, делает .mockReset() / vi.resetAllMocks()
отличным выбором для очистки состояния мока между тестами:
afterEach(() => {
// Убедитесь, что состояние не протекает между моками в тестах.
vi.resetAllMocks()
})
В нашей аналогии с коробкой восстановление мока просто означает выброс коробки.
Метод .mockRestore() фактически отменяет мок, удаляя как записанные вызовы, так и любую смоделированную реализацию. Он актуален только для шпионов (spies). Функция, за которой мы следим (spy), всегда имеет либо нашу смоделированную реализацию, либо оригинальную реализацию, и восстановление этого шпиона означает возвращение функции к её оригинальному, «чистому» состоянию.
Вот как это выглядит в коде:
const spy = vi.spyOn(console, 'log')
console.log(1)
expect(console.log).toHaveBeenCalledTimes(1) // ✅
// Отменяет этот мок, восстанавливая оригинальную реализацию
// отслеживаемой функции (console.log).
spy.mockRestore()
expect(console.log).toHaveBeenCalledTimes(0)
// ❌ TypeError: [Function log] не является шпионом или вызовом шпиона!
Из-за этого .mockRestore() / vi.restoreAllMocks()
являются предпочтительным выбором для очистки после себя в хуках, таких как afterAll:
afterAll(() => {
// Убедитесь, что вы восстановили все моки
// после завершения этого набора тестов.
vi.restoreAllMocks()
})
Давайте подытожим различия между этими методами:
.mockClear()
очищает зарегистрированные вызовы мок-функций;
.mockReset()
очищает зарегистрированные вызовы и удаляет любые реализации моков;
.mockRestore()
удаляет мок, полностью отменяя его.
Поскольку вы никогда не хотите, чтобы состояние мока делилось между тестами, рекомендуется настроить автоматический сброс моков в вашей тестовой среде. В Vitest это делается путем предоставления опции mockReset
в вашем vite.config.ts / vitest.config.ts
:
export default defineConfig({
test: {
mockReset: true
}
})
Вот и всё! Надеюсь, вы запомните это объяснение в следующий раз, когда будете очищать, сбрасывать или восстанавливать ваши моки.