vi.spyOn(объект, 'метод') оборачивает реальный, уже существующий метод объекта: он запоминает вызовы и по умолчанию продолжает вызывать оригинал. Это принципиально отличается от vi.fn(), которая создаёт заглушку на пустом месте. Спай не заменяет поведение сам по себе - он наблюдает за настоящим методом, а замена включается отдельно, только когда вы её просите.
Отсюда разделение: vi.fn нужна для инъектируемой зависимости, которую вы сами передаёте в конструктор или аргумент, а vi.spyOn - для метода реального объекта, который иначе подменить трудно: console.error, Date.now, метод стороннего сервиса. Правило выбора - брать самый узкий инструмент: если зависимость можно передать явно, spyOn не нужен; он для случаев, где точку вызова не вынести наружу.
Первый режим - наблюдать, не заменяя. spyOn оборачивает метод, оригинал по-прежнему выполняется, а тест проверяет и факт вызова, и реальный эффект. В примере спай ставят на service.applyDiscount: настоящая скидка считается, тест убеждается, что метод вызван, и что итоговая сумма верна. По окончании метод возвращают в исходное состояние через mockRestore.
Второй режим - заменить поведение. Поверх спая вызывают .mockReturnValue или .mockImplementation, и метод начинает отдавать заданное вместо настоящего. Классический пример - зафиксировать время: vi.spyOn(Date, 'now').mockReturnValue(...) делает now детерминированным на время теста. После проверки mockRestore возвращает настоящий Date.now, чтобы подмена не утекла дальше.
mockRestore работает только со шпионами - и это ключевое отличие от vi.fn. Спай помнит оригинал и умеет его вернуть; у самостоятельной заглушки восстанавливать нечего. Чтобы не звать mockRestore руками в каждом тесте, включают restoreMocks: true в конфиге - тогда все спаи автоматически восстанавливаются после каждого теста, и утечка подмены становится невозможной по построению.
Шпионить можно и аксессоры: vi.spyOn(объект, 'свойство', 'get') или 'set' оборачивает геттер и сеттер. Это удобно, когда наблюдаемое - обращение к свойству, а не вызов метода. Типичные реальные цели спаев - console.error, чтобы проверить предупреждение, Date.now для времени и методы сервиса, которые нельзя передать через зависимость.
Главная опасность спая - оставить его незакрытым. Если подменить console.error и не восстановить, подмена протечёт в следующие тесты: они либо сломаются на неожиданном моке, либо, наоборот, тихо проглотят настоящие ошибки. Поэтому шпион всегда восстанавливают - через restoreMocks в конфиге или mockRestore в afterEach; незакрытый спай - классическая причина флака, зависящего от порядка.
Итог по выбору: vi.fn - для новой инъектируемой зависимости, vi.spyOn - для наблюдения или замены реального существующего метода, vi.mock - для подмены целого модуля-границы. Начинайте с самого узкого: если хватает передать заглушку через аргумент, не трогайте spyOn; если можно обойтись спаем на один метод, не мокируйте весь модуль. Чем уже вмешательство, тем меньше связь теста с реализацией и тем легче его потом сопровождать.
test('считает скидку и логирует её', () => {
const spy = vi.spyOn(service, 'applyDiscount') // оригинал всё ещё выполняется
const total = service.total(premiumOrder)
expect(spy).toHaveBeenCalledWith(premiumOrder)
expect(total).toBe(8_100) // реальный результат, не заглушка
spy.mockRestore()
})test('ставит время создания заказа', () => {
vi.spyOn(Date, 'now').mockReturnValue(1_700_000_000_000)
const order = createOrder()
expect(order.createdAt).toBe(1_700_000_000_000)
vi.restoreAllMocks() // вернуть настоящий Date.now
})