Skip to content
Vitest 0
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 參考文件

模擬函數 (Mock Functions)

Vi

expect

expectTypeOf

assertType

配置

配置 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 將透過調用它並模擬每個匯出的模組。

以下原則適用:

  • 所有陣列都將被清空
  • 所有原始類型和集合將保持不變
  • 所有物件都將被深度複製
  • 類別的所有實例及其原型都將被深度複製

範例 ​

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,它使用 node-request-interceptor。若要瞭解有關 MSW 的更多資訊,請閱讀他們的 簡介

配置 ​

您可以在您的 設定檔 中像下面這樣使用它

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

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

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

const graphqlHandlers = [
  graphql.query(
    'https://graphql-endpoint.example/api/v1/posts',
    (req, res, ctx) => {
      return res(ctx.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');
  • 模擬導出的函數

使用 vi.mock 的範例:

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

vi.mock('./some-path.js', () => ({
  method: vi.fn(),
}));

WARNING

請注意 vi.mock 的調用會被提升到文件的頂部。 不要 將 vi.mock 調用放在 beforeEach 裡面,只有第一個呼叫會實際生效。

使用 vi.spyOn 的範例:

ts
import * as exports from './some-path.js';

vi.spyOn(exports, 'method').mockImplementation(() => {});
  • 模擬導出的類別實現

使用 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 的實例

使用 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 將會包含返回的物件

使用 vi.spyOn 的範例:

ts
import * as exports from './some-path.js';

vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
  // 任何適合前兩個範例的實作
});
  • 監視從函數返回的物件

使用緩存的範例:

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 () => {
  const mod = await vi.importActual<typeof import('./some-path.js')>(
    './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

要更改環境變數,您可以直接為其賦予一個新值。 這個值不會在不同的測試之間自動重置。

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

如果您想要自動重置值,您可以啟用 unstubEnvs 配置選項來使用 vi.stubEnv 輔助函數(或在 beforeEach hook 中手動調用 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://v0.vitest.dev/guide/mocking

以 MIT 授權條款 發布。

版權所有 (c) 2024 Mithril Contributors