vitest/test/core/test/jest-mock.test.ts

814 lines
20 KiB
TypeScript

import { describe, expect, expectTypeOf, it, vi } from 'vitest'
describe('jest mock compat layer', () => {
const returnFactory = (type: string) => (value: any) => ({ type, value })
const r = returnFactory('return')
const e = returnFactory('throw')
const f = returnFactory('fulfilled')
const h = returnFactory('rejected')
it('works with name', () => {
const spy = vi.fn()
spy.mockName('spy test name')
expect(spy.getMockName()).toBe('spy test name')
})
it('clearing', () => {
const spy = vi.fn()
spy('hello')
expect(spy.mock.calls).toHaveLength(1)
expect(spy.mock.calls[0]).toEqual(['hello'])
spy('world')
expect(spy.mock.calls).toEqual([['hello'], ['world']])
spy.mockReset() // same as mockClear()
expect(spy.mock.calls).toEqual([])
})
it('clearing instances', () => {
const Spy = vi.fn()
expect(Spy.mock.instances).toHaveLength(0)
const _result = new Spy()
expect(Spy.mock.instances).toHaveLength(1)
Spy.mockReset() // same as mockClear()
expect(Spy.mock.instances).toHaveLength(0)
})
it('collects contexts', () => {
// eslint-disable-next-line prefer-arrow-callback
const Spy = vi.fn(function () {})
expect(Spy.mock.contexts).toHaveLength(0)
const ctx = new Spy()
expect(Spy.mock.contexts).toHaveLength(1)
expect(Spy.mock.contexts[0]).toBe(ctx)
Spy.mockReset()
expect(Spy.mock.contexts).toHaveLength(0)
const ctx2 = {}
Spy.call(ctx2)
expect(Spy.mock.contexts).toHaveLength(1)
expect(Spy.mock.contexts[0]).toBe(ctx2)
Spy.bind(ctx2)()
expect(Spy.mock.contexts).toHaveLength(2)
expect(Spy.mock.contexts[1]).toBe(ctx2)
Spy.apply(ctx2)
expect(Spy.mock.contexts).toHaveLength(3)
expect(Spy.mock.contexts[2]).toBe(ctx2)
})
it('tracks spied class contexts and instances', () => {
interface SpyClass {}
interface SpyConstructor {
(): SpyClass
new (): SpyClass
}
const Spy = (function () {}) as SpyConstructor
const obj = { Spy }
const spy = vi.spyOn(obj, 'Spy')
const instance = new obj.Spy()
expectTypeOf(spy.mock.contexts[0]).toEqualTypeOf<SpyClass>()
expect(spy.mock.instances).toEqual([instance])
expect(spy.mock.contexts).toEqual([instance])
})
it('implementation is set correctly on init', () => {
const impl = () => 1
const mock1 = vi.fn(impl)
expect(mock1.getMockImplementation()).toEqual(impl)
const mock2 = vi.fn()
expect(mock2.getMockImplementation()).toBeUndefined()
})
it('implementation types allow only function returned types', () => {
function fn() {
return 1
}
function asyncFn() {
return Promise.resolve(1)
}
const mock1 = vi.fn(fn)
const mock2 = vi.fn(asyncFn)
mock1.mockImplementation(() => 2)
// @ts-expect-error promise is not allowed
mock1.mockImplementation(() => Promise.resolve(2))
// @ts-expect-error non-promise is not allowed
mock2.mockImplementation(() => 2)
mock2.mockImplementation(() => Promise.resolve(2))
})
it('implementation sync fn', () => {
const originalFn = function () {
return 'original'
}
const spy = vi.fn(originalFn)
spy() // returns 'original'
expect(spy.getMockImplementation()).toBe(originalFn)
spy.mockReturnValueOnce('2-once').mockReturnValueOnce('3-once')
spy() // returns '2-once'
spy() // returns '3-once'
spy() // returns 'original'
const implOnce = () => 'once'
spy.mockImplementationOnce(implOnce)
spy() // returns 'once'
spy() // returns 'original'
expect(spy.getMockImplementation() === implOnce).toBe(false) // jest doesn't store Once implementations
const impl = () => 'unlimited'
spy.mockImplementation(impl)
spy() // returns 'unlimited'
spy()
spy()
expect(spy.getMockImplementation() === impl).toBe(true)
spy.mockReturnValue('return-unlimited')
spy()
spy()
expect(spy.mock.results).toEqual([
r('original'),
r('2-once'),
r('3-once'),
r('original'),
r('once'),
r('original'),
r('unlimited'),
r('unlimited'),
r('unlimited'),
r('return-unlimited'),
r('return-unlimited'),
])
spy.mockRestore()
// Sicne Vitest 4 this is a special case where
// vi.fn(impl) will always return impl, unless overriden
expect(spy.getMockImplementation()).toBe(originalFn)
expect(spy.mock.results).toEqual([])
})
it('implementation async fn', async () => {
const originalFn = async function () {
return 'original'
}
const spy = vi.fn(originalFn)
await spy() // returns 'original'
spy
.mockResolvedValue('unlimited')
.mockResolvedValueOnce('3-once')
.mockResolvedValueOnce('4-once')
await spy()
await spy()
await spy()
await spy()
expect(spy.mock.settledResults).toEqual([
f('original'),
f('3-once'),
f('4-once'),
f('unlimited'),
f('unlimited'),
])
})
it('invocationOrder', () => {
const a = vi.fn()
const b = vi.fn()
a()
b()
expect(a.mock.invocationCallOrder[0]).toBeLessThan(b.mock.invocationCallOrder[0])
})
it('should spy on property getter, and mockRestore should restore original descriptor', () => {
const obj = {
get getter() {
return 'original'
},
}
const spy = vi.spyOn(obj, 'getter', 'get')
expect(obj.getter).toBe('original')
spy.mockImplementation(() => 'mocked').mockImplementationOnce(() => 'once')
expect(obj.getter).toBe('once')
expect(obj.getter).toBe('mocked')
expect(obj.getter).toBe('mocked')
spy.mockReturnValue('returned').mockReturnValueOnce('returned-once')
expect(obj.getter).toBe('returned-once')
expect(obj.getter).toBe('returned')
expect(obj.getter).toBe('returned')
spy.mockRestore()
expect(obj.getter).toBe('original')
expect(spy).not.toHaveBeenCalled()
})
it('should spy on property getter, and mockReset should not restore original descriptor', () => {
const obj = {
get getter() {
return 'original'
},
}
const spy = vi.spyOn(obj, 'getter', 'get')
expect(obj.getter).toBe('original')
spy.mockImplementation(() => 'mocked').mockImplementationOnce(() => 'once')
expect(obj.getter).toBe('once')
expect(obj.getter).toBe('mocked')
expect(obj.getter).toBe('mocked')
spy.mockReturnValue('returned').mockReturnValueOnce('returned-once')
expect(obj.getter).toBe('returned-once')
expect(obj.getter).toBe('returned')
expect(obj.getter).toBe('returned')
spy.mockReset()
expect(obj.getter).toBe('original')
expect(spy).toHaveBeenCalled()
})
it('should spy on function returned from property getter', () => {
const obj = {
get getter() {
return function () {
return 'original'
}
},
}
const spy = vi.spyOn(obj, 'getter')
expect(obj.getter()).toBe('original')
spy.mockImplementation(() => 'mocked').mockImplementationOnce(() => 'once')
expect(obj.getter()).toBe('once')
expect(obj.getter()).toBe('mocked')
expect(obj.getter()).toBe('mocked')
})
it('should spy on property setter (1)', () => {
let setValue = 'original'
let mockedValue = 'none'
const obj = {
get setter() {
return setValue
},
set setter(v: any) {
setValue = v
},
}
const spy = vi.spyOn(obj, 'setter', 'set')
obj.setter = 'first'
expect(setValue).toBe('first')
expect(mockedValue).toBe('none')
spy.mockImplementation(() => (mockedValue = 'mocked')).mockImplementationOnce(() => (mockedValue = 'once'))
obj.setter = 'i can do whatever'
expect(mockedValue).toBe('once')
expect(setValue).toBe('first')
obj.setter = 'does nothing'
expect(mockedValue).toBe('mocked')
expect(setValue).toBe('first')
obj.setter = 'since setter is mocked'
expect(mockedValue).toBe('mocked')
expect(setValue).toBe('first')
spy.mockRestore()
obj.setter = 'last'
expect(spy.getMockImplementation()).toBe(undefined)
expect(setValue).toBe('last')
})
it('should spy on property setter (2), and mockRestore should restore original descriptor', () => {
const obj = {
_property: false,
set property(value) {
this._property = value
},
get property() {
return this._property
},
}
const spy = vi.spyOn(obj, 'property', 'set')
obj.property = true
expect(spy).toHaveBeenCalled()
expect(obj.property).toBe(true)
obj.property = false
spy.mockRestore()
obj.property = true
// like jest, mockRestore restores the original descriptor,
// we are not spying on the setter any more
expect(spy).not.toHaveBeenCalled()
expect(obj.property).toBe(true)
})
it('respyin on a spy resets the counter', () => {
const obj = {
method() {
return 'original'
},
}
vi.spyOn(obj, 'method')
obj.method()
expect(obj.method).toHaveBeenCalledTimes(1)
vi.spyOn(obj, 'method')
obj.method()
expect(obj.method).toHaveBeenCalledTimes(2)
})
it('spyOn on the getter multiple times', () => {
const obj = {
get getter() {
return 'original'
},
}
vi.spyOn(obj, 'getter', 'get').mockImplementation(() => 'mocked')
vi.spyOn(obj, 'getter', 'get')
expect(obj.getter).toBe('mocked')
})
it('spyOn multiple times', () => {
const obj = {
method() {
return 'original'
},
}
const spy1 = vi.spyOn(obj, 'method').mockImplementation(() => 'mocked')
const spy2 = vi.spyOn(obj, 'method')
expect(vi.isMockFunction(obj.method)).toBe(true)
expect(obj.method()).toBe('mocked')
expect(spy1).toBe(spy2)
spy2.mockImplementation(() => 'mocked2')
expect(obj.method()).toBe('mocked2')
spy2.mockRestore()
expect(obj.method()).toBe('original')
expect(vi.isMockFunction(obj.method)).toBe(false)
expect(obj.method).not.toBe(spy1)
spy1.mockRestore()
expect(vi.isMockFunction(obj.method)).toBe(false)
expect(obj.method).not.toBe(spy1)
})
it('restoreAllMocks in stack order', () => {
const obj = { foo: () => 'foo' }
vi.spyOn(obj, 'foo').mockImplementation(() => 'mocked1')
expect(obj.foo()).toBe('mocked1')
expect(vi.isMockFunction(obj.foo)).toBe(true)
vi.spyOn(obj, 'foo').mockImplementation(() => 'mocked2')
expect(obj.foo()).toBe('mocked2')
expect(vi.isMockFunction(obj.foo)).toBe(true)
vi.restoreAllMocks()
expect(obj.foo()).toBe('foo')
expect(vi.isMockFunction(obj.foo)).toBe(false)
})
it('should spy on property setter (2), and mockReset should not restore original descriptor', () => {
const obj = {
_property: false,
set property(value) {
this._property = value
},
get property() {
return this._property
},
}
const spy = vi.spyOn(obj, 'property', 'set')
obj.property = true
expect(spy).toHaveBeenCalled()
expect(obj.property).toBe(true)
obj.property = false
spy.mockReset()
obj.property = true
// unlike jest, vitest's mockReset will restore original implementation without restoring the original descriptor.
// We are still spying on the setter
expect(spy).toHaveBeenCalled()
expect(obj.property).toBe(true)
})
it('throwing', async () => {
const fn = vi.fn(() => {
// eslint-disable-next-line no-throw-literal
throw 'error'
})
try {
fn()
}
catch {}
expect(fn.mock.results).toEqual([
e('error'),
])
})
it('mockRejectedValue', async () => {
const safeCall = async (fn: () => void) => {
try {
await fn()
}
catch {}
}
const spy = vi.fn()
.mockRejectedValue(new Error('error'))
.mockRejectedValueOnce(new Error('once'))
await safeCall(spy)
await safeCall(spy)
expect(spy.mock.results[0]).toEqual({
type: 'return',
value: expect.any(Promise),
})
expect(spy.mock.settledResults[0]).toEqual(h(new Error('once')))
expect(spy.mock.settledResults[1]).toEqual(h(new Error('error')))
})
it('mockResolvedValue', async () => {
const spy = vi.fn()
.mockResolvedValue('resolved')
.mockResolvedValueOnce('once')
await spy()
await spy()
expect(spy.mock.results[0]).toEqual({
type: 'return',
value: expect.any(Promise),
})
expect(spy.mock.settledResults[0]).toEqual(f('once'))
expect(spy.mock.settledResults[1]).toEqual(f('resolved'))
})
it('tracks instances made by mocks', () => {
const Fn = vi.fn()
expect(Fn.mock.instances).toEqual([])
const instance1 = new Fn()
expect(Fn.mock.instances[0]).toBe(instance1)
const instance2 = new Fn()
expect(Fn.mock.instances[1]).toBe(instance2)
})
it('.mockRestore() should restore initial implementation', () => {
const testFn = vi.fn(() => true)
expect(testFn()).toBe(true)
testFn.mockReturnValue(false)
expect(testFn()).toBe(false)
testFn.mockRestore()
expect(testFn()).toBe(true)
})
abstract class Dog_ {
public name: string
constructor(name: string) {
this.name = name
}
abstract speak(): string
abstract feed(): void
}
it('mocks constructors', () => {
const Dog = vi.fn<(name: string) => Dog_>(function Dog_(name: string) {
this.name = name
} as (this: any, name: string) => Dog_)
;(Dog as any).getType = vi.fn(() => 'mocked animal')
Dog.prototype.speak = vi.fn(() => 'loud bark!')
Dog.prototype.feed = vi.fn()
const dogMax = new Dog('Max')
expect(dogMax.name).toBe('Max')
expect(dogMax.speak()).toBe('loud bark!')
expect(dogMax.speak).toHaveBeenCalled()
vi.mocked(dogMax.speak).mockReturnValue('woof woof')
expect(dogMax.speak()).toBe('woof woof')
})
it('mock classes', () => {
const Dog = vi.fn(class Dog {
constructor(public name: string) {
this.name = name
}
static getType: () => string = vi.fn(() => 'mocked animal')
speak = vi.fn(() => 'loud bark!')
feed = vi.fn()
})
const dogMax = new Dog('Max')
expect(dogMax.name).toBe('Max')
expect(dogMax.speak()).toBe('loud bark!')
expect(dogMax.speak).toHaveBeenCalled()
vi.mocked(dogMax.speak).mockReturnValue('woof woof')
expect(dogMax.speak()).toBe('woof woof')
})
it('spies on classes', () => {
class Example {
test() {}
}
const obj = {
Example,
}
const Spy = vi.spyOn(obj, 'Example')
Spy.mockImplementation(() => {})
expect(() => new Spy()).toThrowError(TypeError)
Spy.mockImplementation(function () {
expectTypeOf(this.test).toEqualTypeOf<() => void>()
this.test = () => {}
})
expect(new Spy()).toBeInstanceOf(Spy)
class MockExample {
test() {}
}
Spy.mockImplementation(MockExample)
expect(new Spy()).toBeInstanceOf(Spy)
expect(new Spy()).not.toBeInstanceOf(MockExample)
const instance = new Spy()
expectTypeOf(instance).toEqualTypeOf<Example>()
})
it('returns temporary implementations from getMockImplementation()', () => {
const fn = vi.fn()
const temporaryMockImplementation = () => 'mocked value'
fn.mockImplementationOnce(temporaryMockImplementation)
expect(fn.getMockImplementation()).toBe(temporaryMockImplementation)
// After calling it, it should be back to undefined
fn()
expect(fn.getMockImplementation()).toBe(undefined)
const mockImplementation = () => 'other mocked value'
fn.mockImplementation(mockImplementation)
expect(fn.getMockImplementation()).toBe(mockImplementation)
// It should also overwrite permanent implementations
fn.mockImplementationOnce(temporaryMockImplementation)
expect(fn.getMockImplementation()).toBe(temporaryMockImplementation)
})
it('keeps the descriptor the same as the original one when restoring', () => {
class Foo {
f() {
return 'original'
}
}
// initially there's no own properties
const foo = new Foo()
expect(foo.f()).toMatchInlineSnapshot(`"original"`)
expect(Object.getOwnPropertyDescriptors(foo)).toMatchInlineSnapshot(`{}`)
// mocked function in own property
const spy = vi.spyOn(foo, 'f').mockImplementation(() => 'mocked')
expect(foo.f()).toMatchInlineSnapshot(`"mocked"`)
expect(Object.getOwnPropertyDescriptors(foo)).toMatchInlineSnapshot(`
{
"f": {
"configurable": true,
"enumerable": false,
"value": [MockFunction f] {
"calls": [
[],
],
"results": [
{
"type": "return",
"value": "mocked",
},
],
},
"writable": true,
},
}
`)
// probably original prototype method is not moved to own property
spy.mockRestore()
expect(foo.f()).toMatchInlineSnapshot(`"original"`)
expect(Object.getOwnPropertyDescriptors(foo)).toMatchInlineSnapshot(`{}`)
})
it('mocks inherited methods', () => {
class Bar {
_bar = 'bar'
get bar(): string {
return this._bar
}
set bar(bar: string) {
this._bar = bar
}
}
class Foo extends Bar {}
const foo = new Foo()
vi.spyOn(foo, 'bar', 'get').mockImplementation(() => 'foo')
expect(foo.bar).toEqual('foo')
// foo.bar setter is inherited from Bar, so we can set it
expect(() => {
foo.bar = 'baz'
}).not.toThrowError()
expect(foo.bar).toEqual('foo')
})
it('mocks inherited overridden methods', () => {
class Bar {
_bar = 'bar'
get bar(): string {
return this._bar
}
set bar(bar: string) {
this._bar = bar
}
}
class Foo extends Bar {
get bar(): string {
return `${super.bar}-foo`
}
}
const foo = new Foo()
expect(foo.bar).toEqual('bar-foo')
vi.spyOn(foo, 'bar', 'get').mockImplementation(() => 'foo')
expect(foo.bar).toEqual('foo')
// foo.bar setter is not inherited from Bar
expect(() => {
// @ts-expect-error bar is readonly
foo.bar = 'baz'
}).toThrowError()
expect(foo.bar).toEqual('foo')
})
describe('is disposable', () => {
describe.runIf(Symbol.dispose)('in environments supporting it', () => {
it('has dispose property', () => {
expect(vi.fn()[Symbol.dispose]).toBeTypeOf('function')
})
it('calls mockRestore when disposing', () => {
const fn = vi.fn()
const restoreSpy = vi.spyOn(fn, 'mockRestore')
{
using _fn2 = fn
}
expect(restoreSpy).toHaveBeenCalled()
})
it('allows disposal when using mockImplementation', () => {
expect(vi.fn().mockImplementation(() => {})[Symbol.dispose]).toBeTypeOf('function')
})
})
describe.skipIf(Symbol.dispose)('in environments not supporting it', () => {
it('does not have dispose property', () => {
expect(vi.fn()[Symbol.dispose]).toBeUndefined()
})
})
})
describe('docs example', () => {
it('mockClear', () => {
const person = {
greet: (name: string) => `Hello ${name}`,
}
const spy = vi.spyOn(person, 'greet').mockImplementation(() => 'mocked')
expect(person.greet('Alice')).toBe('mocked')
expect(spy.mock.calls).toEqual([['Alice']])
// clear call history but keep mock implementation
spy.mockClear()
expect(spy.mock.calls).toEqual([])
expect(person.greet('Bob')).toBe('mocked')
expect(spy.mock.calls).toEqual([['Bob']])
})
it('mockReset', () => {
const person = {
greet: (name: string) => `Hello ${name}`,
}
const spy = vi.spyOn(person, 'greet').mockImplementation(() => 'mocked')
expect(person.greet('Alice')).toBe('mocked')
expect(spy.mock.calls).toEqual([['Alice']])
// clear call history and reset implementation, but method is still spied
spy.mockReset()
expect(spy.mock.calls).toEqual([])
expect(person.greet).toBe(spy)
expect(person.greet('Bob')).toBe('Hello Bob')
expect(spy.mock.calls).toEqual([['Bob']])
})
it('mockRestore', () => {
const person = {
greet: (name: string) => `Hello ${name}`,
}
const spy = vi.spyOn(person, 'greet').mockImplementation(() => 'mocked')
expect(person.greet('Alice')).toBe('mocked')
expect(spy.mock.calls).toEqual([['Alice']])
// clear call history and restore spied object method
spy.mockRestore()
expect(spy.mock.calls).toEqual([])
expect(person.greet).not.toBe(spy)
expect(person.greet('Bob')).toBe('Hello Bob')
expect(spy.mock.calls).toEqual([])
})
})
})