モック
テストを作成する際、内部または外部サービスのモックバージョンが必要になることはよくあります。これは一般的にモッキングと呼ばれます。Vitest は、vi ヘルパーを通じて、このモッキングを支援するユーティリティ関数を提供しています。import { vi } from 'vitest'
でインポートするか、(グローバルオプションが有効な場合) グローバル変数としてアクセスできます。
WARNING
各テストの実行前または実行後に、モックをリセットまたは復元し、実行間でモックの状態が変更されないようにしてください。詳細については、mockReset
のドキュメントを参照してください。
すぐに使い始めたい場合は、API セクションを参照してください。そうでなければ、このまま読み進めてモックについてより深く理解しましょう。
日付
テストの一貫性を保つために、日付を制御する必要がある場合があります。Vitest は、タイマーとシステム日付を操作するために、@sinonjs/fake-timers
パッケージを使用します。特定の API の詳細については、こちらを参照してください。
例
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' });
});
});
関数
関数のモックは、スパイとモックという 2 つの異なるカテゴリに分類できます。
特定の関数が呼び出されたかどうか (および、場合によってはどの引数が渡されたか) を検証するだけでよい場合があります。このような場合、スパイは vi.spyOn()
で直接使用できる必要な機能です (詳細はこちらを参照してください)。
ただし、スパイは関数の監視に役立つだけでなく、これらの関数の実装を変更することはできません。関数のモックバージョンを作成する必要がある場合は、vi.fn()
を使用できます (詳細はこちらを参照してください)。
関数をモックするためのベースとして Tinyspy を使用していますが、jest
との互換性を持たせるために独自のラッパーを用意しています。vi.fn()
と vi.spyOn()
はどちらも同じメソッドを共有していますが、vi.fn()
の戻り値のみが呼び出し可能です。
例
import { afterEach, beforeEach, 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);
});
});
詳しくはこちら
グローバル変数
vi.stubGlobal
ヘルパーを使用すると、jsdom
または node
に存在しないグローバル変数をモックできます。これは、グローバル変数の値を globalThis
オブジェクトに設定します。
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 はモジュール自体を呼び出し、すべてのエクスポートをモックすることで、モジュール自体をモックします。
次の規則が適用されます。
- すべての配列は空になります
- すべてのプリミティブとコレクションはそのままになります
- すべてのオブジェクトは深いコピーされます
- クラスのすべてのインスタンスとそのプロトタイプは深いコピーされます
例
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({});
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 の詳細については、概要を参照してください。
構成
セットアップファイルで以下のように使用できます。
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);
// すべてのテストの前にサーバーを起動
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
// すべてのテストの後にサーバーを停止
afterAll(() => server.close());
// 各テストの後にハンドラーをリセット (テストの分離に重要)
afterEach(() => server.resetHandlers());
onUnhandleRequest: 'error'
でサーバーを構成すると、対応するハンドラがないリクエストがあるたびにエラーがスローされるようになります。
例
MSW を使用した完全に動作する例として、MSW を使用した React テストがあります。
詳しくはこちら
MSW にはさらに多くの機能があります。Cookie とクエリパラメータにアクセスしたり、モックエラー応答を定義したりできます。MSW でできることすべてを確認するには、ドキュメントを参照してください。
タイマー
タイムアウトやインターバルを含むコードをテストする場合、テストが完了するのを待ったり、タイムアウトさせたりする代わりに、setTimeout
および setInterval
の呼び出しをモックする「偽の」タイマーを使用することで、テストを高速化できます。
詳細な API の説明については、vi.useFakeTimers
API セクションを参照してください。
例
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
function executeAfterTwoHours(func) {
setTimeout(func, 1000 * 60 * 60 * 2);
}
function executeEveryMinute(func) {
setInterval(func, 1000 * 60);
}
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);
});
});
チートシート
INFO
以下の例の vi
は、vitest
から直接インポートされています。 設定 で globals
を true
に設定すると、グローバル変数として使用することも可能です。
次のことを行いたい
method
をスパイする
const instance = new SomeClass();
vi.spyOn(instance, 'method');
- エクスポートされた変数をモックする
// some-path.js
export const getter = 'variable';
// some-path.test.ts
import * as exports from './some-path.js';
vi.spyOn(exports, 'getter', 'get').mockReturnValue('mocked');
- エクスポートされた関数をモックする
vi.mock
を使用した例:
// ./some-path.js
export function method() {}
import { method } from './some-path.js';
vi.mock('./some-path.js', () => ({
method: vi.fn(),
}));
WARNING
vi.mock
の呼び出しはファイルの先頭にホイストされることに注意してください。 vi.mock
の呼び出しを beforeEach
ブロックの中に記述しないでください。記述した場合、最初に実行された vi.mock
のみがモジュールをモックします。
vi.spyOn
を使用した例:
import * as exports from './some-path.js';
vi.spyOn(exports, 'method').mockImplementation(() => {});
- エクスポートされたクラスの実装をモックする
vi.mock
とプロトタイプを使用した例:
// some-path.ts
export class SomeClass {}
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
と戻り値を使用した例:
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
を使用した例:
import * as exports from './some-path.js';
vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
// 最初の二つの例から適切なものを選択してください
});
- 関数から返されたオブジェクトをスパイする
キャッシュを使用した例:
// some-path.ts
export function useObject() {
return { method: () => true };
}
// useObject.js
import { useObject } from './some-path.js';
const obj = useObject();
obj.method();
// 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();
- モジュールの一部をモックする
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(); // スパイ関数
- 現在の日付をモックする
Date
オブジェクトの時間をモックするには、vi.setSystemTime
ヘルパー関数を使用できます。この値は、テスト間で自動的にリセットされません。
vi.useFakeTimers
を使用すると、Date
オブジェクトの時間も変更されることに注意してください。
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
を呼び出さない限り、テスト間で自動的にリセットされません。
vi.stubGlobal('__VERSION__', '1.0.0');
expect(__VERSION__).toBe('1.0.0');
import.meta.env
をモックする
環境変数を変更するには、新しい値を代入します。この値は、テスト間で自動的にリセットされません。
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');
});
値を自動的にリセットしたい場合は、unstubEnvs
設定オプションを有効にして vi.stubEnv
ヘルパーを使用してください。または、beforeEach
フックで vi.unstubAllEnvs
を手動で呼び出すこともできます。
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');
});
// vitest.config.ts
export default {
test: {
unstubAllEnvs: true,
},
};