跳至内容

模拟

在编写测试时,迟早您需要创建一个内部或外部服务的“伪造”版本。这通常被称为 **模拟**。Vitest 通过其 **vi** 助手提供实用程序函数来帮助您。您可以 import { vi } from 'vitest' 或在 **全局** 访问它(当 全局配置 **启用** 时)。

警告

始终记住在每次测试运行之前或之后清除或恢复模拟,以撤消运行之间模拟状态的更改!有关更多信息,请参阅 mockReset 文档。

如果您想直接深入,请查看 API 部分,否则请继续阅读以更深入地了解模拟的世界。

日期

有时您需要控制日期以确保测试时的一致性。Vitest 使用 @sinonjs/fake-timers 包来操作计时器以及系统日期。您可以在 此处 详细了解特定 API。

示例

js
import { , , , , ,  } from 'vitest'

const  = [9, 17]

function () {
  const  = new ().()
  const [, ] = 

  if ( >  &&  < )
    return { : 'Success' }

  return { : 'Error' }
}

('purchasing flow', () => {
  (() => {
    // tell vitest we use mocked time
    .()
  })

  (() => {
    // restoring date after each test run
    .()
  })

  ('allows purchases within business hours', () => {
    // set hour within business hours
    const  = new (2000, 1, 1, 13)
    .()

    // access Date.now() will result in the date set above
    (()).({ : 'Success' })
  })

  ('disallows purchases outside of business hours', () => {
    // set hour outside business hours
    const  = new (2000, 1, 1, 19)
    .()

    // access Date.now() will result in the date set above
    (()).({ : 'Error' })
  })
})

函数

模拟函数可以分为两个不同的类别:间谍和模拟

有时您只需要验证是否调用了特定函数(以及可能传递了哪些参数)。在这些情况下,间谍将是我们所需要的,您可以直接使用 vi.spyOn() (在此处阅读更多信息)。

但是间谍只能帮助您 **监视** 函数,它们无法更改这些函数的实现。在我们需要创建函数的伪造(或模拟)版本的情况下,我们可以使用 vi.fn() (在此处阅读更多信息)。

我们使用 Tinyspy 作为模拟函数的基础,但我们有自己的包装器使其与 jest 兼容。vi.fn()vi.spyOn() 共享相同的方法,但是只有 vi.fn() 的返回值是可调用的。

示例

js
import { , , , ,  } from 'vitest'

function ( = .. - 1) {
  return .[]
}

const  = {
  : [
    { : 'Simple test message', : 'Testman' },
    // ...
  ],
  , // can also be a `getter or setter if supported`
}

('reading messages', () => {
  (() => {
    .()
  })

  ('should get the latest message with a spy', () => {
    const  = .(, 'getLatest')
    (.()).('getLatest')

    (.()).(
      .[.. - 1],
    )

    ().(1)

    .(() => 'access-restricted')
    (.()).('access-restricted')

    ().(2)
  })

  ('should get with a mock', () => {
    const  = .().()

    (()).(.[.. - 1])
    ().(1)

    .(() => 'access-restricted')
    (()).('access-restricted')

    ().(2)

    (()).(.[.. - 1])
    ().(3)
  })
})

更多

全局变量

您可以使用 vi.stubGlobal 助手模拟 jsdomnode 中不存在的全局变量。它会将全局变量的值放入 globalThis 对象中。

ts
import {  } from 'vitest'

const  = .(() => ({
  : .(),
  : .(),
  : .(),
  : .(),
}))

.('IntersectionObserver', )

// now you can access it as `IntersectionObserver` or `window.IntersectionObserver`

模块

模拟模块观察在其他代码中调用的第三方库,允许您测试参数、输出甚至重新声明其实现。

有关更深入的详细 API 说明,请参阅 vi.mock() API 部分

自动模拟算法

如果您的代码正在导入模拟模块,但没有为此模块关联的 __mocks__ 文件或 factory,Vitest 将通过调用它并模拟每个导出来模拟模块本身。

以下原则适用

  • 所有数组将被清空
  • 所有基本类型和集合将保持不变
  • 所有对象将被深度克隆
  • 所有类实例及其原型将被深度克隆

虚拟模块

Vitest 支持模拟 Vite 虚拟模块。它的工作方式与 Jest 中处理虚拟模块的方式不同。您无需将 virtual: true 传递给 vi.mock 函数,而是需要告诉 Vite 该模块存在,否则它将在解析期间失败。您可以通过多种方式做到这一点

  1. 提供别名
ts
// vitest.config.js
export default {
  test: {
    alias: {
      '$app/forms': resolve('./mocks/forms.js')
    }
  }
}
  1. 提供一个解析虚拟模块的插件
ts
// vitest.config.js
export default {
  plugins: [
    {
      name: 'virtual-modules',
      resolveId(id) {
        if (id === '$app/forms')
          return 'virtual:$app/forms'
      }
    }
  ]
}

第二种方法的好处是您可以动态创建不同的虚拟入口点。如果您将多个虚拟模块重定向到单个文件,那么它们都将受到 vi.mock 的影响,因此请确保使用唯一的标识符。

模拟陷阱

请注意,无法模拟对在同一文件中的其他方法内部调用的方法的调用。例如,在此代码中

ts
export function foo() {
  return 'foo'
}

export function foobar() {
  return `${foo()}bar`
}

无法从外部模拟 foo 方法,因为它被直接引用。因此,此代码对 foobar 内部的 foo 调用没有影响(但会影响其他模块中的 foo 调用)

ts
import { vi } from 'vitest'
import * as mod from './foobar.js'

// this will only affect "foo" outside of the original module
vi.spyOn(mod, 'foo')
vi.mock('./foobar.js', async (importOriginal) => {
  return {
    ...await importOriginal<typeof import('./foobar.js')>(),
    // this will only affect "foo" outside of the original module
    foo: () => 'mocked'
  }
})

您可以通过直接向 foobar 方法提供实现来确认此行为

ts
// foobar.test.js
import * as mod from './foobar.js'

vi.spyOn(mod, 'foo')

// exported foo references mocked method
mod.foobar(mod.foo)
ts
// foobar.js
export function foo() {
  return 'foo'
}

export function foobar(injectedFoo) {
  return injectedFoo !== foo // false
}

这是预期的行为。当以这种方式进行模拟时,通常是代码不好的迹象。考虑将您的代码重构为多个文件,或通过使用 依赖注入 等技术来改进您的应用程序架构。

示例

js
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { Client } from 'pg'
import { failure, success } from './handlers.js'

// handlers
export function success(data) {}
export function failure(data) {}

// get todos
export async function getTodos(event, context) {
  const client = new Client({
    // ...clientOptions
  })

  await client.connect()

  try {
    const result = await client.query('SELECT * FROM todos;')

    client.end()

    return success({
      message: `${result.rowCount} item(s) returned`,
      data: result.rows,
      status: true,
    })
  }
  catch (e) {
    console.error(e.stack)

    client.end()

    return failure({ message: e, status: false })
  }
}

vi.mock('pg', () => {
  const Client = vi.fn()
  Client.prototype.connect = vi.fn()
  Client.prototype.query = vi.fn()
  Client.prototype.end = vi.fn()

  return { Client }
})

vi.mock('./handlers.js', () => {
  return {
    success: vi.fn(),
    failure: vi.fn(),
  }
})

describe('get a list of todo items', () => {
  let client

  beforeEach(() => {
    client = new Client()
  })

  afterEach(() => {
    vi.clearAllMocks()
  })

  it('should return items successfully', async () => {
    client.query.mockResolvedValueOnce({ rows: [], rowCount: 0 })

    await getTodos()

    expect(client.connect).toBeCalledTimes(1)
    expect(client.query).toBeCalledWith('SELECT * FROM todos;')
    expect(client.end).toBeCalledTimes(1)

    expect(success).toBeCalledWith({
      message: '0 item(s) returned',
      data: [],
      status: true,
    })
  })

  it('should throw an error', async () => {
    const mError = new Error('Unable to retrieve rows')
    client.query.mockRejectedValueOnce(mError)

    await getTodos()

    expect(client.connect).toBeCalledTimes(1)
    expect(client.query).toBeCalledWith('SELECT * FROM todos;')
    expect(client.end).toBeCalledTimes(1)
    expect(failure).toBeCalledWith({ message: mError, status: false })
  })
})

请求

由于 Vitest 在 Node 中运行,因此模拟网络请求很棘手;Web API 不可用,因此我们需要一些东西来模拟我们的网络行为。我们建议使用 Mock Service Worker 来完成此操作。它将允许您模拟 RESTGraphQL 网络请求,并且与框架无关。

Mock Service Worker (MSW) 通过拦截您的测试发出的请求来工作,允许您在不更改任何应用程序代码的情况下使用它。在浏览器中,这使用 Service Worker API。在 Node.js 和 Vitest 中,它使用 @mswjs/interceptors 库。要了解有关 MSW 的更多信息,请阅读他们的 介绍

配置

您可以在您的 设置文件 中像下面这样使用它

js
import { , ,  } from 'vitest'
import {  } from 'msw/node'
import { , ,  } from 'msw'

const  = [
  {
    : 1,
    : 1,
    : 'first post title',
    : 'first post body',
  },
  // ...
]

export const  = [
  .get('https://rest-endpoint.example/path/to/posts', () => {
    return .json()
  }),
]

const  = [
  .query('ListPosts', () => {
    return .json(
      {
        : {  },
      },
    )
  }),
]

const  = (..., ...)

// Start server before all tests
(() => .listen({ : 'error' }))

//  Close server after all tests
(() => .close())

// Reset handlers after each test `important for test isolation`
(() => .resetHandlers())

使用 onUnhandleRequest: 'error' 配置服务器可确保在没有相应请求处理程序的请求时抛出错误。

示例

我们有一个完整的示例,它使用 MSW:使用 MSW 进行 React 测试

更多

MSW 还有更多功能。您可以访问 cookie 和查询参数,定义模拟错误响应等等!要查看 MSW 的所有功能,请阅读 他们的文档

计时器

当我们测试涉及超时或间隔的代码时,我们可以通过使用模拟对 setTimeoutsetInterval 的调用的“伪造”计时器来加快我们的测试速度,而不是让我们的测试等待或超时。

有关更深入的详细 API 说明,请参阅 vi.useFakeTimers API 部分

示例

js
import { , , , , ,  } from 'vitest'

function () {
  (, 1000 * 60 * 60 * 2) // 2 hours
}

function () {
  (, 1000 * 60) // 1 minute
}

const  = .(() => .('executed'))

('delayed execution', () => {
  (() => {
    .()
  })
  (() => {
    .()
  })
  ('should execute the function', () => {
    ()
    .()
    ().(1)
  })
  ('should not execute the function', () => {
    ()
    // advancing by 2ms won't trigger the func
    .(2)
    ()..()
  })
  ('should execute every minute', () => {
    ()
    .()
    ().(1)
    .()
    ().(2)
  })
})

备忘单

信息

以下示例中的 vi 是直接从 vitest 导入的。您也可以在全局使用它,如果您在您的 配置 中将 globals 设置为 true

我想…

监视 method

ts
const instance = new SomeClass()
vi.spyOn(instance, 'method')

模拟导出的变量

js
// some-path.js
export const getter = 'variable'
ts
// some-path.test.ts
import * as exports from './some-path.js'

vi.spyOn(exports, 'getter', 'get').mockReturnValue('mocked')

模拟导出的函数

  1. 使用 vi.mock 的示例

警告

不要忘记 vi.mock 调用被提升到文件顶部。它将始终在所有导入之前执行。

ts
// ./some-path.js
export function method() {}
ts
import { method } from './some-path.js'

vi.mock('./some-path.js', () => ({
  method: vi.fn()
}))
  1. 使用 vi.spyOn 的示例
ts
import * as exports from './some-path.js'

vi.spyOn(exports, 'method').mockImplementation(() => {})

模拟导出类实现

  1. 使用 vi.mock.prototype 的示例
ts
// some-path.ts
export class SomeClass {}
ts
import { SomeClass } from './some-path.js'

vi.mock('./some-path.js', () => {
  const SomeClass = vi.fn()
  SomeClass.prototype.someMethod = vi.fn()
  return { SomeClass }
})
// SomeClass.mock.instances will have SomeClass
  1. 使用 vi.mock 和返回值的示例
ts
import { SomeClass } from './some-path.js'

vi.mock('./some-path.js', () => {
  const SomeClass = vi.fn(() => ({
    someMethod: vi.fn()
  }))
  return { SomeClass }
})
// SomeClass.mock.returns will have returned object
  1. 使用 vi.spyOn 的示例
ts
import * as exports from './some-path.js'

vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
  // whatever suites you from first two examples
})

监听从函数返回的对象

  1. 使用缓存的示例
ts
// some-path.ts
export function useObject() {
  return { method: () => true }
}
ts
// useObject.js
import { useObject } from './some-path.js'

const obj = useObject()
obj.method()
ts
// useObject.test.js
import { useObject } from './some-path.js'

vi.mock('./some-path.js', () => {
  let _cache
  const useObject = () => {
    if (!_cache) {
      _cache = {
        method: vi.fn(),
      }
    }
    // now every time that useObject() is called it will
    // return the same object reference
    return _cache
  }
  return { useObject }
})

const obj = useObject()
// obj.method was called inside some-path
expect(obj.method).toHaveBeenCalled()

模拟模块的一部分

ts
import { mocked, original } from './some-path.js'

vi.mock('./some-path.js', async (importOriginal) => {
  const mod = await importOriginal<typeof import('./some-path.js')>()
  return {
    ...mod,
    mocked: vi.fn()
  }
})
original() // has original behaviour
mocked() // is a spy function

模拟当前日期

要模拟 Date 的时间,可以使用 vi.setSystemTime 辅助函数。此值 **不会** 在不同测试之间自动重置。

注意,使用 vi.useFakeTimers 也会更改 Date 的时间。

ts
const mockDate = new Date(2022, 0, 1)
vi.setSystemTime(mockDate)
const now = new Date()
expect(now.valueOf()).toBe(mockDate.valueOf())
// reset mocked time
vi.useRealTimers()

模拟全局变量

您可以通过将值分配给 globalThis 或使用 vi.stubGlobal 辅助函数来设置全局变量。使用 vi.stubGlobal 时,它 **不会** 在不同测试之间自动重置,除非您启用 unstubGlobals 配置选项或调用 vi.unstubAllGlobals

ts
vi.stubGlobal('__VERSION__', '1.0.0')
expect(__VERSION__).toBe('1.0.0')

模拟 import.meta.env

  1. 要更改环境变量,您可以直接为其分配一个新值。

警告

环境变量值 **不会** 在不同测试之间自动重置。

ts
import { beforeEach, expect, it } from 'vitest'

// you can reset it in beforeEach hook manually
const originalViteEnv = import.meta.env.VITE_ENV

beforeEach(() => {
  import.meta.env.VITE_ENV = originalViteEnv
})

it('changes value', () => {
  import.meta.env.VITE_ENV = 'staging'
  expect(import.meta.env.VITE_ENV).toBe('staging')
})
  1. 如果您希望自动重置值,可以使用 vi.stubEnv 辅助函数,并启用 unstubEnvs 配置选项(或在 beforeEach 钩子中手动调用 vi.unstubAllEnvs
ts
import { expect, it, vi } from 'vitest'

// before running tests "VITE_ENV" is "test"
import.meta.env.VITE_ENV === 'test'

it('changes value', () => {
  vi.stubEnv('VITE_ENV', 'staging')
  expect(import.meta.env.VITE_ENV).toBe('staging')
})

it('the value is restored before running an other test', () => {
  expect(import.meta.env.VITE_ENV).toBe('test')
})
ts
// vitest.config.ts
export default {
  test: {
    unstubAllEnvs: true,
  }
}