对象模拟(Mocking)

在编写测试时,你可能会因为时间问题,需要创建内部或外部服务的“假”版本,这通常被称为对象模拟操作。Vitest 通过 vi 提供了一些实用的函数用于解决这个问题。你可以使用 import { vi } from 'vitest' 或者全局配置进行访问它 (当启用全局配置时)。

::: warning 警告 不要忘记在每次测试运行前后清除或恢复模拟对象,以撤消运行测试时模拟对象状态的更改!有关更多信息,请参阅 mockReset 文档。 :::

如果你想先深入了解, 可以先阅读 API 的 vi 部分,或者继续跟着文档深入了解一下这个对象模拟的世界。

日期

有些时候,你可能需要控制日期来确保测试时的一致性。Vitest 使用了 @sinonjs/fake-timers 库来操作定时器以及系统日期。可以在这里找到有关特定 API 的更多详细信息。

示例

  1. import { afterEach, describe, expect, it, vi } from 'vitest'
  2. const businessHours = [9, 17]
  3. const purchase = () => {
  4. const currentHour = new Date().getHours()
  5. const [open, close] = businessHours
  6. if (currentHour > open && currentHour < close) {
  7. return { message: 'Success' }
  8. }
  9. return { message: 'Error' }
  10. };
  11. describe('purchasing flow', () => {
  12. beforeEach(() => {
  13. // 告诉 vitest 我们使用模拟时间
  14. vi.useFakeTimers()
  15. });
  16. afterEach(() => {
  17. // 每次测试运行后恢复日期
  18. vi.useRealTimers()
  19. });
  20. it('allows purchases within business hours', () => {
  21. // 在营业时间内设定时间
  22. const date = new Date(2000, 1, 1, 13)
  23. vi.setSystemTime(date)
  24. // 访问 Date.now() 将导致上面设置的日期
  25. expect(purchase()).toEqual({ message: 'Success' })
  26. })
  27. it('disallows purchases outside of business hours', () => {
  28. // 设置营业时间以外的时间
  29. const date = new Date(2000, 1, 1, 19)
  30. vi.setSystemTime(date)
  31. // 访问 Date.now() 将导致上面设置的日期
  32. expect(purchase()).toEqual({ message: 'Error' })
  33. })
  34. })

函数

对于函数的模拟可以分为两种类别:对象监听(Spy)对象模拟.

有时你可能只需要验证是否调用了特定函数(以及可能传递了哪些参数),在这种情况下,我们就需要使用一个对象监听,可以直接使用 vi.spyOn() 来对一个函数进行监听(在这里可以阅读更多的内容)。

然而对象监听操作没有办法去更改这些函数的实现。当我们需要创建一个可以使用的假的(或模拟的)函数版本时,可以使用 vi.fn()在这里可以阅读更多的内容)。

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

示例

  1. import { afterEach, describe, expect, it, vi } from 'vitest'
  2. const getLatest = (index = messages.items.length - 1) => messages.items[index]
  3. const messages = {
  4. items: [
  5. { message: 'Simple test message', from: 'Testman' },
  6. // ...
  7. ],
  8. getLatest, // 也可以是“getter 或 setter(如果支持)”
  9. describe('reading messages', () => {
  10. afterEach(() => {
  11. vi.restoreAllMocks()
  12. })
  13. it('should get the latest message with a spy', () => {
  14. const spy = vi.spyOn(messages, 'getLatest')
  15. expect(spy.getMockName()).toEqual('getLatest')
  16. expect(messages.getLatest()).toEqual(
  17. messages.items[messages.items.length - 1]
  18. );
  19. expect(spy).toHaveBeenCalledTimes(1)
  20. spy.mockImplementationOnce(() => 'access-restricted')
  21. expect(messages.getLatest()).toEqual('access-restricted')
  22. expect(spy).toHaveBeenCalledTimes(2)
  23. });
  24. it('should get with a mock', () => {
  25. const mock = vi.fn().mockImplementation(getLatest)
  26. expect(mock()).toEqual(messages.items[messages.items.length - 1])
  27. expect(mock).toHaveBeenCalledTimes(1)
  28. mock.mockImplementationOnce(() => 'access-restricted')
  29. expect(mock()).toEqual('access-restricted')
  30. expect(mock).toHaveBeenCalledTimes(2)
  31. expect(mock()).toEqual(messages.items[messages.items.length - 1])
  32. expect(mock).toHaveBeenCalledTimes(3)
  33. })
  34. })

了解更多

模块

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

参见 vi.mock() API 部分,以获得更深入详细 API 描述。

自动模拟算法

如果你的代码导入了模拟模块,但是没有使用 __mocks__ 文件或 factory 模块,Vitest 将会通过调用和模拟模块的每个导出来模拟模块本身。

适用于以下这些原则:

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

示例

  1. import { vi, beforeEach, afterEach, describe, it } from 'vitest';
  2. import { Client } from 'pg';
  3. // handlers
  4. export function success(data) {}
  5. export function failure(data) {}
  6. // get todos
  7. export const getTodos = async (event, context) => {
  8. const client = new Client({
  9. // ...clientOptions
  10. });
  11. await client.connect()
  12. try {
  13. const result = await client.query(`SELECT * FROM todos;`)
  14. client.end()
  15. return success({
  16. message: `${result.rowCount} item(s) returned`,
  17. data: result.rows,
  18. status: true,
  19. })
  20. } catch (e) {
  21. console.error(e.stack)
  22. client.end()
  23. return failure({ message: e, status: false })
  24. }
  25. };
  26. vi.mock('pg', () => {
  27. return {
  28. Client: vi.fn(() => ({
  29. connect: vi.fn(),
  30. query: vi.fn(),
  31. end: vi.fn(),
  32. })),
  33. };
  34. });
  35. vi.mock('./handler.js', () => {
  36. return {
  37. success: vi.fn(),
  38. failure: vi.fn(),
  39. };
  40. });
  41. describe('get a list of todo items', () => {
  42. let client;
  43. beforeEach(() => {
  44. client = new Client()
  45. });
  46. afterEach(() => {
  47. vi.clearAllMocks()
  48. });
  49. it('should return items successfully', async () => {
  50. client.query.mockResolvedValueOnce({ rows: [], rowCount: 0 })
  51. await getTodos()
  52. expect(client.connect).toBeCalledTimes(1)
  53. expect(client.query).toBeCalledWith('SELECT * FROM todos;')
  54. expect(client.end).toBeCalledTimes(1)
  55. expect(success).toBeCalledWith({
  56. message: '0 item(s) returned',
  57. data: [],
  58. status: true,
  59. })
  60. })
  61. it('should throw an error', async () => {
  62. const mError = new Error('Unable to retrieve rows')
  63. client.query.mockRejectedValueOnce(mError)
  64. await getTodos()
  65. expect(client.connect).toBeCalledTimes(1)
  66. expect(client.query).toBeCalledWith('SELECT * FROM todos;')
  67. expect(client.end).toBeCalledTimes(1)
  68. expect(failure).toBeCalledWith({ message: mError, status: false })
  69. })
  70. })

网络请求

因为 Vitest 运行在 Node 环境中,所以模拟网络请求是一件非常棘手的事情; 由于没有办法使用 Web API,因此我们需要一些可以为我们模拟网络行为的包。推荐使用 Mock Service Worker 来进行这个操作。它可以同时模拟 RESTGraphQL 网络请求,并且跟所使用的框架没有任何联系。

Mock Service Worker (MSW) 通过拦截测试发出的请求进行工作,允许我们在不更改任何应用程序代码的情况下使用它。在浏览器中,会使用 Service Worker API。在 Node 和 Vitest 中,会使用 node-request-interceptor。要了解有关 MSW 的更多信息,可以去阅读他们的介绍

配置

将以下内容添加到测试配置文件

  1. import { beforeAll, afterAll, afterEach } from 'vitest'
  2. import { setupServer } from 'msw/node'
  3. import { graphql, rest } from 'msw'
  4. const posts = [
  5. {
  6. userId: 1,
  7. id: 1,
  8. title: 'first post title',
  9. body: 'first post body',
  10. },
  11. ...
  12. ]
  13. export const restHandlers = [
  14. rest.get('https://rest-endpoint.example/path/to/posts', (req, res, ctx) => {
  15. return res(ctx.status(200), ctx.json(posts))
  16. }),
  17. ]
  18. const graphqlHandlers = [
  19. graphql.query('https://graphql-endpoint.example/api/v1/posts', (req, res, ctx) => {
  20. return res(ctx.data(posts))
  21. }),
  22. ]
  23. const server = setupServer(...restHandlers, ...graphqlHandlers)
  24. // 在所有测试之前启动服务器
  25. beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
  26. // 所有测试后关闭服务器
  27. afterAll(() => server.close())
  28. // 每次测试后重置处理程序“对测试隔离很重要”
  29. afterEach(() => server.resetHandlers())

配置服务 onUnhandleRequest: 'error' 只要产生了没有相应类型的请求处理,就会发生错误。

示例

我们有一个使用 MSW 的完整工作示例: React Testing with MSW.

了解更多

MSW还有很多。您可以访问 cookie 和查询参数、定义模拟错误响应等等!要查看您可以使用 MSW 做什么,请阅读他们的文档

定时器

每当我们测试涉及到超时或者间隔的代码时,并不是让我们的测试程序进行等待或者超时。我们也可以通过模拟对 setTimeoutsetInterval 的调用来使用“假”定时器来加速我们的测试。

有关更深入的详细 API 描述,请参vi.mock() API 部分

示例

  1. import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest'
  2. const executeAfterTwoHours = (func) => {
  3. setTimeout(func, 1000 * 60 * 60 * 2) // 2 hours
  4. }
  5. const executeEveryMinute = (func) => {
  6. setInterval(func, 1000 * 60); // 1 minute
  7. }
  8. const mock = vi.fn(() => console.log('executed'));
  9. describe('delayed execution', () => {
  10. beforeEach(() => {
  11. vi.useFakeTimers()
  12. })
  13. afterEach(()=> {
  14. vi.restoreAllMocks()
  15. })
  16. it('should execute the function', () => {
  17. executeAfterTwoHours(mock);
  18. vi.runAllTimers();
  19. expect(mock).toHaveBeenCalledTimes(1);
  20. })
  21. it('should not execute the function', () => {
  22. executeAfterTwoHours(mock);
  23. // advancing by 2ms won't trigger the func
  24. vi.advanceTimersByTime(2);
  25. expect(mock).not.toHaveBeenCalled();
  26. })
  27. it('should execute every minute', () => {
  28. executeEveryMinute(mock);
  29. vi.advanceTimersToNextTimer();
  30. vi.advanceTimersToNextTimer();
  31. expect(mock).toHaveBeenCalledTimes(1);
  32. vi.advanceTimersToNextTimer();
  33. expect(mock).toHaveBeenCalledTimes(2);
  34. })
  35. })