如何使用 Jest 和 TypeScript 监视 ES6 类的方法?

kYu*_*uZz 7 typescript jestjs ts-jest

我正在尝试监视我手动模拟的 ES6 类的方法。

我正在关注官方 Jest 文档,该文档仅针对 JavaScript。

这是我的代码:

// sound-player.ts
export default class SoundPlayer {
  foo: string

  constructor() {
    this.foo = 'bar'
  }

  playFile(fileName: string) {
    console.log(`Playing sound file: ${fileName}`)
  }
}
Run Code Online (Sandbox Code Playgroud)
// __mocks__/sound-player.ts
export const mockPlayFile = jest.fn()

const mock = jest.fn().mockImplementation(() => {
  return { playFile: mockPlayFile }
})

export default mock
Run Code Online (Sandbox Code Playgroud)
// sound-player-consumer.ts
import SoundPlayer from './sound-player'

export default class SoundPlayerConsumer {
  soundPlayer: SoundPlayer

  constructor(soundPlayer: SoundPlayer) {
    this.soundPlayer = soundPlayer
  }

  playSomethingCool() {
    this.soundPlayer.playFile('something-cool.mp3')
  }
}
Run Code Online (Sandbox Code Playgroud)
// sound-player-consumer.test.ts
// @ts-ignore
import SoundPlayer, { mockPlayFile } from './sound-player'
import SoundPlayerConsumer from './sound-player-consumer'

jest.mock('./sound-player')

const MockedSoundPlayer = SoundPlayer as jest.Mock<SoundPlayer>

beforeEach(() => {
  MockedSoundPlayer.mockClear()
  mockPlayFile.mockClear()
})

it('We can check if the consumer called the class constructor', () => {
  const soundPlayer = new MockedSoundPlayer()
  const soundPlayerConsumer = new SoundPlayerConsumer(soundPlayer)
  expect(SoundPlayer).toHaveBeenCalledTimes(1)
})

it('We can check if the consumer called a method on the class instance', () => {
  const soundPlayer = new MockedSoundPlayer()
  const soundPlayerConsumer = new SoundPlayerConsumer(soundPlayer)
  soundPlayerConsumer.playSomethingCool()
  expect(mockPlayFile).toHaveBeenCalledWith('something-cool.mp3')
})
Run Code Online (Sandbox Code Playgroud)

这些测试通过了,因为我使用了一个技巧。

请注意第一行sound-player-consumer.test.ts。如果没有这个,TypeScript 编译器会抱怨mockPlayFile没有被sound-player.ts.

对我来说,这意味着从 Jest 的角度来看一切都很好。

不过,我想从 TypeScript 的角度修复所有问题并删除@ts-ignore注释。

Nic*_*yme 3

是的,这有点棘手和尴尬。

// @ts-ignore
import SoundPlayer, { mockPlayFile } from './sound-player'
Run Code Online (Sandbox Code Playgroud)

这里需要注释的原因是因为仅调用实际模块值(即 javascript)而不会重新连接@ts-ignore类型本身。jest.mock('./sound-player')

这仍然有效的原因@ts-ignore是因为使用mock,导入任何东西./sound-player就像从 导入一样__mocks__/sound-player.ts,减去类型。

那么如何解决这个问题呢?您可以mockPlayFile直接从__mocks__文件导入。这将使用实际类型来mockPlayFile减轻对@ts-ignore.

// sound-player-consumer.test.ts
import SoundPlayer from './sound-player'
import { mockPlayFile } from './__mocks__/sound-player'
import SoundPlayerConsumer from './sound-player-consumer'

jest.mock('./sound-player')

// ...
Run Code Online (Sandbox Code Playgroud)

或者,您也可以不导入mockPlayFile并使用类实例方法(即soundPlayer.mockPlayFile)。

it('We can check if the consumer called a method on the class instance', () => {
  const soundPlayer = new MockedSoundPlayer()
  const soundPlayerConsumer = new SoundPlayerConsumer(soundPlayer)
  soundPlayerConsumer.playSomethingCool()
  expect(soundPlayer.mockPlayFile).toHaveBeenCalledWith('something-cool.mp3')
})
Run Code Online (Sandbox Code Playgroud)

请注意,这仅是可能的,因为您在 中的类模拟实现之外定义mockPlayFile为模拟。与定义对类模拟实现的每次调用相反,在这种情况下,模拟函数将有所不同。jest.fn__mocks__/sound-player.tsjest.fn


边注

Jest 允许通过直接导出模拟类实现来手动模拟类,请参阅此处的文档示例。在你的情况下,你可以这样做...

// __mocks__/sound-player.ts
export const mockPlayFile = jest.fn();

export default class SoundPlayer {
  foo: string = 'bar';

  constructor () {
    console.log('mocked SoundPlayer constructor');
  }

  playFile = mockPlayFile;
}
Run Code Online (Sandbox Code Playgroud)

我还没有测试所有的IRL,但我在过去几天做了类似的事情。LMK 如果有问题,我可以提供一个工作示例,可惜https://codesandbox.io不支持jest.mock.