Skip to content
Vitest 1
Main Navigation 指南API配置高级
1.6.1
0.34.6

简体中文

English
繁體中文
Español
Français
Русский
Português – Brasil
Deutsch
日本語
한국어
Italiano
Polski
Türkçe
čeština
magyar

简体中文

English
繁體中文
Español
Français
Русский
Português – Brasil
Deutsch
日本語
한국어
Italiano
Polski
Türkçe
čeština
magyar

主题

Sidebar Navigation

指南

为什么选择 Vitest

快速开始

特性

工作区

命令行界面

测试筛选

报告器

代码覆盖率

快照(Snapshot)

模拟(Mocking)

类型测试

Vitest UI

浏览器模式

源码内测试

测试上下文

测试环境

扩展匹配器

IDE 集成

调试

与其他测试运行器的比较

迁移指南

常见错误

提升性能

API

测试 API 索引

模拟函数

Vi

expect

expectTypeOf

assert(断言)

assertType

配置

管理 Vitest 配置文件

配置 Vitest

页面导航

模拟(Mocking) ​

在编写测试时,经常需要在测试过程中创建内部或外部服务的“模拟”版本。这通常被称为“模拟 (mocking)”。Vitest 提供了实用函数,可以通过 vi 辅助函数来帮助你。你可以 import { vi } from 'vitest' 或者全局访问它(当 全局配置 被启用时)。

WARNING

务必在每次测试运行前后清除或恢复模拟,以撤销不同运行之间的状态更改!有关更多信息,请参见 mockReset 文档。

如需快速了解,可以查看 API 部分;否则请继续阅读以更深入地探索模拟的世界。

日期 ​

在某些情况下,需要控制日期以确保测试时的一致性。Vitest 使用 @sinonjs/fake-timers 包来操作定时器以及系统日期。详细 API 文档请参考此处。

示例 ​

js
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const businessHours = [9, 17];

function purchase() {
  const currentHour = new Date().getHours();
  const [open, close] = businessHours;

  if (currentHour > open && currentHour < close) return { message: 'Success' };

  return { message: 'Error' };
}

describe('purchasing flow', () => {
  beforeEach(() => {
    // 告诉 Vitest 我们使用模拟时间
    vi.useFakeTimers();
  });

  afterEach(() => {
    // 在每次测试运行后恢复日期
    vi.useRealTimers();
  });

  it('allows purchases within business hours', () => {
    // 设置在工作时间内的 hour
    const date = new Date(2000, 1, 1, 13);
    vi.setSystemTime(date);

    // 访问 Date.now() 将返回上面设置的日期
    expect(purchase()).toEqual({ message: 'Success' });
  });

  it('disallows purchases outside of business hours', () => {
    // 设置在工作时间外的 hour
    const date = new Date(2000, 1, 1, 19);
    vi.setSystemTime(date);

    // 访问 Date.now() 将返回上面设置的日期
    expect(purchase()).toEqual({ message: 'Error' });
  });
});

函数 ​

模拟函数可分为两类:间谍 (spying) 和模拟 (mocking)。

有时你只需要验证是否已调用特定函数(以及可能传递了哪些参数)。此时使用间谍 (spy) 即可,你可以直接使用 vi.spyOn() (在此处阅读更多信息)。

但间谍仅能用于监视函数调用,无法更改这些函数的实现。如果我们需要创建一个函数的伪造(或模拟)版本,我们可以使用 vi.fn() (在此处阅读更多信息)。

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

示例 ​

js
import { afterEach, describe, expect, it, vi } from 'vitest';

function getLatest(index = messages.items.length - 1) {
  return messages.items[index];
}

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

describe('reading messages', () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('should get the latest message with a spy', () => {
    const spy = vi.spyOn(messages, 'getLatest');
    expect(spy.getMockName()).toEqual('getLatest');

    expect(messages.getLatest()).toEqual(
      messages.items[messages.items.length - 1]
    );

    expect(spy).toHaveBeenCalledTimes(1);

    spy.mockImplementationOnce(() => 'access-restricted');
    expect(messages.getLatest()).toEqual('access-restricted');

    expect(spy).toHaveBeenCalledTimes(2);
  });

  it('should get with a mock', () => {
    const mock = vi.fn().mockImplementation(getLatest);

    expect(mock()).toEqual(messages.items[messages.items.length - 1]);
    expect(mock).toHaveBeenCalledTimes(1);

    mock.mockImplementationOnce(() => 'access-restricted');
    expect(mock()).toEqual('access-restricted');

    expect(mock).toHaveBeenCalledTimes(2);

    expect(mock()).toEqual(messages.items[messages.items.length - 1]);
    expect(mock).toHaveBeenCalledTimes(3);
  });
});

更多 ​

  • Jest 的模拟函数

全局变量 ​

你可以使用 vi.stubGlobal 辅助函数来模拟在 jsdom 或 node 中不存在的全局变量。它会将全局变量的值放入 globalThis 对象中。

ts
import { vi } from 'vitest';

const IntersectionObserverMock = vi.fn(() => ({
  disconnect: vi.fn(),
  observe: vi.fn(),
  takeRecords: vi.fn(),
  unobserve: vi.fn(),
}));

vi.stubGlobal('IntersectionObserver', IntersectionObserverMock);

// 现在你可以通过 `IntersectionObserver` 或 `window.IntersectionObserver` 访问它

模块 ​

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

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

自动模拟算法 ​

当代码导入被模拟的模块时,但没有相关的 __mocks__ 文件或该模块的 factory(工厂函数),Vitest 将通过调用它并模拟每个导出 (export) 来模拟模块本身。

以下原则适用:

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

虚拟模块 ​

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';

// 这只会影响原始模块外部的 "foo"
vi.spyOn(mod, 'foo');
vi.mock('./foobar.js', async importOriginal => {
  return {
    ...(await importOriginal<typeof import('./foobar.js')>()),
    // 这只会影响原始模块外部的 "foo"
    foo: () => 'mocked',
  };
});

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

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

vi.spyOn(mod, 'foo');

// 导出的 foo 引用模拟方法
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';

// 处理成功/失败函数
export function success(data) {}
export function failure(data) {}

// 获取待办事项
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 中运行,因此模拟网络请求非常棘手;浏览器 API 不可用,因此我们需要一些可以模拟网络行为的东西。我们推荐使用 Mock Service Worker 来完成此操作。它将允许你模拟 REST 和 GraphQL 网络请求,并且与框架无关。

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

配置 ​

你可以在你的 setup file(设置文件)中像下面这样使用它:

js
import { afterAll, afterEach, beforeAll } from 'vitest';
import { setupServer } from 'msw/node';
import { HttpResponse, graphql, http } from 'msw';

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

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

const graphqlHandlers = [
  graphql.query('ListPosts', () => {
    return HttpResponse.json({
      data: { posts },
    });
  }),
];

const server = setupServer(...restHandlers, ...graphqlHandlers);

// 在所有测试之前启动服务器
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));

// 在所有测试之后关闭服务器
afterAll(() => server.close());

// 在每次测试后重置处理程序,这对测试隔离很重要
afterEach(() => server.resetHandlers());

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

示例 ​

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

更多 ​

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

定时器 ​

当我们测试涉及超时或间隔的代码时,与其让测试等待或超时,不如使用模拟 setTimeout 和 setInterval 调用的“伪造”定时器来加速测试。

有关更深入的详细 API 描述,请参见 vi.useFakeTimers api 部分。

示例 ​

js
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

function executeAfterTwoHours(func) {
  setTimeout(func, 1000 * 60 * 60 * 2); // 2 hours
}

function executeEveryMinute(func) {
  setInterval(func, 1000 * 60); // 1 minute
}

const mock = vi.fn(() => console.log('executed'));

describe('delayed execution', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });
  afterEach(() => {
    vi.restoreAllMocks();
  });
  it('should execute the function', () => {
    executeAfterTwoHours(mock);
    vi.runAllTimers();
    expect(mock).toHaveBeenCalledTimes(1);
  });
  it('should not execute the function', () => {
    executeAfterTwoHours(mock);
    // 提前 2 毫秒不会触发该函数
    vi.advanceTimersByTime(2);
    expect(mock).not.toHaveBeenCalled();
  });
  it('should execute every minute', () => {
    executeEveryMinute(mock);
    vi.advanceTimersToNextTimer();
    expect(mock).toHaveBeenCalledTimes(1);
    vi.advanceTimersToNextTimer();
    expect(mock).toHaveBeenCalledTimes(2);
  });
});

备忘单 ​

INFO

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

我想要…

监视 (Spy) 一个方法 method ​

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

模拟 (Mock) 导出的变量 ​

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');

模拟 (Mock) 导出的函数 ​

  1. 使用 vi.mock 的示例:

WARNING

请注意,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(() => {});

模拟 (Mock) 导出的类的实现 ​

  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 将会包含 SomeClass 的实例
  1. 使用 vi.mock 和返回值(return value)的示例:
ts
import { SomeClass } from './some-path.js';

vi.mock('./some-path.js', () => {
  const SomeClass = vi.fn(() => ({
    someMethod: vi.fn(),
  }));
  return { SomeClass };
});
// SomeClass.mock.results 将会包含返回的对象
  1. 使用 vi.spyOn 的示例:
ts
import * as exports from './some-path.js';

vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
  // 可根据前两个示例中的任一方式实现
});

监视 (Spy) 从函数返回的对象 ​

  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(),
      };
    }
    // 现在每次调用 useObject() 时,它将
    // 返回相同的对象引用
    return _cache;
  };
  return { useObject };
});

const obj = useObject();
// obj.method 在 some-path 内部被调用
expect(obj.method).toHaveBeenCalled();

模拟 (Mock) 模块的一部分 ​

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(); // 具有原始行为
mocked(); // 是一个 spy 函数

模拟 (Mock) 当前日期 ​

要模拟 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());
// 重置模拟时间
vi.useRealTimers();

模拟 (Mock) 全局变量 ​

你可以通过为 globalThis 赋值或使用 vi.stubGlobal 辅助函数来设置全局变量。当使用 vi.stubGlobal 时,除非你启用 unstubGlobals 配置选项或调用 vi.unstubAllGlobals,否则它不会在不同的测试之间自动重置。

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

模拟 (Mock) import.meta.env ​

  1. 想要改变环境变量,你可以直接给它赋一个新值。

WARNING

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

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

// 你可以在 `beforeEach` 钩子中手动重置它
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';

// 在运行测试之前,`VITE_ENV` 是 `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,
  },
};
Pager
上一页快照(Snapshot)
下一页类型测试

基于 MIT 许可证 发布。

版权所有 (c) 2024 Mithril Contributors

https://v1.vitest.dev/guide/mocking

基于 MIT 许可证 发布。

版权所有 (c) 2024 Mithril Contributors