Skip to content
Vitest 2
Main Navigation 指南API配置浏览器模式高级
2.1.9
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 集成

调试

与其他测试运行器的比较

迁移指南

常见错误

Profiling Test Performance

提升性能

页面导航

模拟 (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', () => {
    // 设置营业时间内的时段
    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', () => {
    // 设置营业时间外的时段
    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';

const messages = {
  items: [
    { message: 'Simple test message', from: 'Testman' },
    // ...
  ],
  getLatest, // 如果支持,也可以是 `getter 或 setter` 方法
};

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

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 将通过调用该模块并模拟其所有导出项来模拟模块本身。

以下原则适用:

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

虚拟模块 ​

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 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 不提供任何内置的文件系统模拟 API。您可以使用 vi.mock 手动模拟 fs 模块,但这很难维护。相反,我们建议使用 memfs 来实现此目的。memfs 创建一个内存文件系统,它模拟文件系统操作而无需触及实际磁盘。这种方法快速且安全,避免对真实文件系统产生任何潜在的副作用。

示例 ​

要自动将每个 fs 调用重定向到 memfs,您可以在项目根目录创建 __mocks__/fs.cjs 和 __mocks__/fs/promises.cjs 文件:

ts
// 我们也可以使用 `import`,但这样
// 每个导出都应该明确定义

const { fs } = require('memfs');
module.exports = fs;
ts
// 我们也可以使用 `import`,但这样
// 每个导出都应该明确定义

const { fs } = require('memfs');
module.exports = fs.promises;
ts
// read-hello-world.js
import { readFileSync } from 'node:fs';

export function readHelloWorld(path) {
  return readFileSync(path);
}
ts
// hello-world.test.js
import { beforeEach, expect, it, vi } from 'vitest';
import { fs, vol } from 'memfs';
import { readHelloWorld } from './read-hello-world.js';

// 告诉 Vitest 使用 __mocks__ 文件夹中的 fs 模拟
// 如果 fs 应该始终被模拟,这可以在 setup 文件中完成
vi.mock('node:fs');
vi.mock('node:fs/promises');

beforeEach(() => {
  // 重置内存文件系统的状态
  vol.reset();
});

it('should return correct text', () => {
  const path = '/hello-world.txt';
  fs.writeFileSync(path, 'hello world');

  const text = readHelloWorld(path);
  expect(text).toBe('hello world');
});

it('can return a value multiple times', () => {
  // 您可以使用 vol.fromJSON 定义多个文件
  vol.fromJSON(
    {
      './dir1/hw.txt': 'hello dir1',
      './dir2/hw.txt': 'hello dir2',
    },
    // 默认 cwd
    '/tmp'
  );

  expect(readHelloWorld('/tmp/dir1/hw.txt')).toBe('hello dir1');
  expect(readHelloWorld('/tmp/dir2/hw.txt')).toBe('hello dir2');
});

请求 ​

由于 Vitest 在 Node 中运行,模拟网络请求比较复杂;Web API 不可用,因此我们需要一些工具来模拟网络行为。我们建议使用 Mock Service Worker 来实现此目的。它将允许您模拟 REST 和 GraphQL 网络请求,并且与框架无关。

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

配置 ​

您可以在设置文件中按如下方式使用它:

js
import { afterAll, afterEach, beforeAll } from 'vitest';
import { setupServer } from 'msw/node';
import { graphql, http, HttpResponse } 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 还有更多功能。您可以访问 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 小时
}

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

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);
    // 推进 2ms 不会触发函数
    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);
  });
});

类 ​

您可以通过单个 vi.fn 调用来模拟整个类——因为所有类也都是函数,所以这可以直接使用。请注意,目前 Vitest 不支持 new 关键字,因此函数体中的 new.target 始终是 undefined。

ts
class Dog {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  static getType(): string {
    return 'animal';
  }

  speak(): string {
    return 'bark!';
  }

  isHungry() {}
  feed() {}
}

我们可以使用 ES5 函数重新创建这个类:

ts
const Dog = vi.fn(function (name) {
  this.name = name;
});

// 注意静态方法是直接在函数上模拟的,
// 而不是在类的实例上
Dog.getType = vi.fn(() => 'mocked animal');

// 模拟类的每个实例上的 "speak" 和 "feed" 方法
// 所有 `new Dog()` 实例都将继承这些 spy
Dog.prototype.speak = vi.fn(() => 'loud bark!');
Dog.prototype.feed = vi.fn();

何时使用?

一般来说,如果类是从另一个模块重新导出的,您会在模块工厂中重新创建此类:

ts
import { Dog } from './dog.js';

vi.mock(import('./dog.js'), () => {
  const Dog = vi.fn();
  Dog.prototype.feed = vi.fn();
  // ... 其他模拟
  return { Dog };
});

此方法还可以用于将类的实例传递给接受相同接口的函数:

ts
// ./src/feed.ts
function feed(dog: Dog) {
  // ...
}

// ./tests/dog.test.ts
import { expect, test, vi } from 'vitest';
import { feed } from '../src/feed.js';

const Dog = vi.fn();
Dog.prototype.feed = vi.fn();

test('can feed dogs', () => {
  const dogMax = new Dog('Max');

  feed(dogMax);

  expect(dogMax.feed).toHaveBeenCalled();
  expect(dogMax.isHungry()).toBe(false);
});

现在,当我们创建 Dog 类的新实例时,它的 speak 方法(以及 feed)已经被模拟了:

ts
const dog = new Dog('Cooper');
dog.speak(); // loud bark!

// 您可以使用内置断言来检查调用的有效性
expect(dog.speak).toHaveBeenCalled();

我们可以为特定实例重新分配返回值:

ts
const dog = new Dog('Cooper');

// "vi.mocked" 是一个类型辅助工具,因为
// TypeScript 不知道 Dog 是一个被模拟的类,
// 它会将任何函数包装成 MockInstance<T> 类型
// 而不验证该函数是否为模拟函数
vi.mocked(dog.speak).mockReturnValue('woof woof');

dog.speak(); // woof woof

要模拟属性,我们可以使用 vi.spyOn(dog, 'name', 'get') 方法。这使得可以在模拟属性上使用 spy 断言:

ts
const dog = new Dog('Cooper');

const nameSpy = vi.spyOn(dog, 'name', 'get').mockReturnValue('Max');

expect(dog.name).toBe('Max');
expect(nameSpy).toHaveBeenCalledTimes(1);

TIP

您也可以使用相同的方法侦察 getter 和 setter。

速查表 (Cheat Sheet) ​

INFO

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

我想…

模拟导出的变量 ​

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 示例:

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(() => {});

模拟导出的类实现 ​

  1. vi.mock 和 .prototype 示例:
ts
// ./some-path.ts
export class SomeClass {}
ts
import { SomeClass } from './some-path.js';

vi.mock(import('./some-path.js'), () => {
  const SomeClass = vi.fn();
  SomeClass.prototype.someMethod = vi.fn();
  return { SomeClass };
});
// SomeClass.mock.instances 将是 SomeClass 的实例
  1. vi.spyOn 示例:
ts
import * as mod from './some-path.js';

const SomeClass = vi.fn();
SomeClass.prototype.someMethod = vi.fn();

vi.spyOn(mod, 'SomeClass').mockImplementation(SomeClass);

侦察从函数返回的对象 ​

  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(import('./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();

模拟模块的一部分 ​

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

vi.mock(import('./some-path.js'), async importOriginal => {
  const mod = await importOriginal();
  return {
    ...mod,
    mocked: vi.fn(),
  };
});
original(); // 具有原始行为
mocked(); // 是一个侦察函数

WARNING

请注意,这只模拟_外部_访问。在此示例中,如果 original 在内部调用 mocked,它将始终调用模块中定义的函数,而不是模拟工厂中的函数。

模拟当前日期 ​

要模拟 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();

模拟全局变量 ​

您可以通过将值分配给 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. 要更改环境变量,您只需为其分配一个新值。

WARNING

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

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

// 在运行测试之前,"VITE_ENV" 的值为 "test"
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('该值在运行另一个测试之前已恢复', () => {
  expect(import.meta.env.VITE_ENV).toBe('test');
});
ts
// vitest.config.ts
export default defineConfig({
  test: {
    unstubEnvs: true,
  },
});
Pager
上一页快照(Snapshot)
下一页类型测试

基于 MIT 许可证 发布。

版权所有 (c) 2024 Mithril Contributors

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

基于 MIT 许可证 发布。

版权所有 (c) 2024 Mithril Contributors