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

開始使用

功能特性

工作區

命令列界面

測試過濾器

報告器

覆蓋率

快照

模擬(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(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('allows purchases within business hours', () => {
    const date = new Date(2000, 1, 1, 13);
    vi.setSystemTime(date);

    expect(purchase()).toEqual({ message: 'Success' });
  });

  it('disallows purchases outside of business hours', () => {
    const date = new Date(2000, 1, 1, 19);
    vi.setSystemTime(date);

    expect(purchase()).toEqual({ message: 'Error' });
  });
});

函式 ​

模擬函式可以分為兩個不同的類別:偵測(spying) 和 模擬(mocking)。

有時您只需要驗證是否已呼叫特定函式(以及可能傳遞了哪些引數)。在這些情況下,間諜(spy)將是您需要的,您可以使用 vi.spyOn() (在此處閱讀更多)。

然而,偵測(spy)只能幫助您**偵測(spy)**函式,無法變更這些函式的實作。如果我們需要建立函式的假(或模擬)版本,我們可以使用 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, // 也可以是 `如果支援 getter 或 setter 的話`
};

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

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

// 這只會影響原始模組外部的 "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';

// 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 來完成此操作。它將讓您模擬 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 { 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);

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

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

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

使用 onUnhandleRequest: 'error' 配置伺服器可確保在沒有相應請求處理常式的請求時拋出錯誤。

範例 ​

我們有一個使用 MSW 的完整工作範例:使用 MSW 進行 React 測試。

更多 ​

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);
    // advancing by 2ms won't trigger the func
    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 匯入的。 如果您在 config 中將 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 的範例:

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('./some-path.js', () => {
  const SomeClass = vi.fn();
  SomeClass.prototype.someMethod = vi.fn();
  return { SomeClass };
});
// SomeClass.mock.instances 將會包含 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 將會包含返回的物件
  1. 使用 vi.spyOn 的範例:
ts
import * as exports from './some-path.js';

vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
  // 任何適合前兩個範例的實作
});

監視從函數返回的物件 ​

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

模擬模組的一部分 ​

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 函式

模擬目前日期 ​

要模擬 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';

// 您可以在 beforeEach hook 中手動重置它
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
上一頁快照
下一頁測試類型

以 MIT 授權條款 發布。

版權所有 (c) 2024 Mithril Contributors

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

以 MIT 授權條款 發布。

版權所有 (c) 2024 Mithril Contributors