モック
テストを作成する際、内部サービスや外部サービスの「偽の」バージョンを作成する必要が生じることがあります。これは一般的にモックと呼ばれます。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, 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);
});
});
その他
グローバル変数
jsdom
やnode
に存在しないグローバル変数をモックするには、vi.stubGlobal
ヘルパーを使用できます。これにより、グローバル変数の値が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はモジュール自体を呼び出し、すべてのエクスポートをモックします。
以下の原則が適用されます。
- すべての配列は空になります
- すべてのプリミティブとコレクションは変更されません
- すべてのオブジェクトはディープクローンされます
- クラスのすべてのインスタンスとそのプロトタイプはディープクローンされます
仮想モジュール
VitestはViteの仮想モジュールのモックをサポートしています。これはJestでの仮想モジュールの扱いとは異なります。vi.mock
関数にvirtual: true
を渡す代わりに、モジュールが存在することをViteに伝える必要があります。これを怠ると、パース中に失敗します。これにはいくつかの方法があります。
- エイリアスを提供する
// vitest.config.js
export default {
test: {
alias: {
'$app/forms': resolve('./mocks/forms.js'),
},
},
};
- 仮想モジュールを解決するプラグインを提供する
// vitest.config.js
export default {
plugins: [
{
name: 'virtual-modules',
resolveId(id) {
if (id === '$app/forms') {
return 'virtual:$app/forms';
}
},
},
],
};
2番目のアプローチの利点は、異なる仮想エントリポイントを動的に作成できる点です。複数の仮想モジュールを単一のファイルにリダイレクトすると、それらすべてがvi.mock
の影響を受けるため、一意の識別子を使用するようにしてください。
モックの落とし穴
同じファイルの他のメソッド内で呼び出されるメソッドの呼び出しをモックすることはできませんので注意してください。たとえば、このコードでは:
export function foo() {
return 'foo';
}
export function foobar() {
return `${foo()}bar`;
}
foo
メソッドは直接参照されているため、外部からモックすることはできません。したがって、このコードはfoobar
内のfoo
呼び出しには影響しません(ただし、他のモジュール内のfoo
呼び出しには影響します):
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
メソッドに直接実装を提供することで確認できます。
// foobar.test.js
import * as mod from './foobar.js';
vi.spyOn(mod, 'foo');
// エクスポートされたfooはモックされたメソッドを参照する
mod.foobar(mod.foo);
// foobar.js
export function foo() {
return 'foo';
}
export function foobar(injectedFoo) {
return injectedFoo === foo; // false
}
これは意図された動作です。モックがこのように使われる場合、通常は悪いコードの兆候です。コードを複数のファイルにリファクタリングするか、依存性注入などの手法を使用してアプリケーションアーキテクチャを改善することを検討してください。
例
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Client } from 'pg';
import { failure, success } from './handlers.js';
// 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は、ファイルシステムモックAPIを標準では提供していません。vi.mock
を使用してfs
モジュールを手動でモックすることはできますが、保守が困難です。代わりに、memfs
を使用することをお勧めします。memfs
はインメモリファイルシステムを作成し、実際のディスクに触れることなくファイルシステム操作をシミュレートします。このアプローチは高速で安全であり、実際のファイルシステムへの潜在的な副作用を回避できます。
例
すべてのfs
呼び出しをmemfs
に自動的にリダイレクトするには、プロジェクトのルートに__mocks__/fs.cjs
と__mocks__/fs/promises.cjs
ファイルを作成します。
// `import`も使用できますが、その場合
// すべてのエクスポートを明示的に定義する必要があります
const { fs } = require('memfs');
module.exports = fs;
// `import`も使用できますが、その場合
// すべてのエクスポートを明示的に定義する必要があります
const { fs } = require('memfs');
module.exports = fs.promises;
// read-hello-world.js
import { readFileSync } from 'node:fs';
export function readHelloWorld(path) {
return readFileSync(path);
}
// 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が常にモックされるべき場合は、セットアップファイルでこれを行うことができる
vi.mock('node:fs');
vi.mock('node:fs/promises');
beforeEach(() => {
// インメモリfsの状態をリセットする
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の詳細については、入門を参照してください。
設定
セットアップファイルで以下のように使用できます。
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には他にも多くの機能があります。クッキーやクエリパラメータにアクセスしたり、モックエラーレスポンスを定義したり、その他にも多くのことができます!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); // 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
であることに注意してください。
class Dog {
name: string;
constructor(name: string) {
this.name = name;
}
static getType(): string {
return 'animal';
}
speak(): string {
return 'bark!';
}
isHungry() {}
feed() {}
}
このクラスをES5関数で再作成できます。
const Dog = vi.fn(function (name) {
this.name = name;
});
// 静的メソッドは関数に直接モックされることに注意してください。
// クラスのインスタンスにはモックされません
Dog.getType = vi.fn(() => 'mocked animal');
// クラスのすべてのインスタンスで"speak"および"feed"メソッドをモックする
// すべての`new Dog()`インスタンスはこれらのスパイを継承する
Dog.prototype.speak = vi.fn(() => 'loud bark!');
Dog.prototype.feed = vi.fn();
いつ使うか?
一般的に、クラスが別のモジュールから再エクスポートされている場合、モジュールファクトリ内でこのようにクラスを再作成することになります。
import { Dog } from './dog.js';
vi.mock(import('./dog.js'), () => {
const Dog = vi.fn();
Dog.prototype.feed = vi.fn();
// ... その他のモック
return { Dog };
});
このメソッドは、同じインターフェースを受け取る関数にクラスのインスタンスを渡すためにも使用できます。
// ./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
とともに)はすでにモックされています。
const dog = new Dog('Cooper');
dog.speak(); // loud bark!
// 組み込みのアサーションを使用して呼び出しの有効性を確認できる
expect(dog.speak).toHaveBeenCalled();
特定のインスタンスの戻り値を再割り当てできます。
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')
メソッドを使用できます。これにより、モックされたプロパティに対してスパイのアサーションを使用することができます。
const dog = new Dog('Cooper');
const nameSpy = vi.spyOn(dog, 'name', 'get').mockReturnValue('Max');
expect(dog.name).toBe('Max');
expect(nameSpy).toHaveBeenCalledTimes(1);
TIP
同じメソッドを使用して、ゲッターとセッターをスパイすることもできます。
チートシート
INFO
以下の例のvi
はvitest
から直接インポートされています。configでglobals
をtrue
に設定すると、グローバルに利用できます。
以下の操作を行いたい場合…
エクスポートされた変数をモックする
// 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
の例:
WARNING
vi.mock
の呼び出しはファイルの先頭に巻き上げられることに注意してください。常にすべてのインポートの前に実行されます。
// ./some-path.js
export function method() {}
import { method } from './some-path.js';
vi.mock('./some-path.js', () => ({
method: vi.fn(),
}));
vi.spyOn
の例:
import * as exports from './some-path.js';
vi.spyOn(exports, 'method').mockImplementation(() => {});
エクスポートされたクラスの実装をモックする
vi.mock
と.prototype
の例:
// ./some-path.ts
export class SomeClass {}
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が含まれる
vi.spyOn
の例:
import * as mod from './some-path.js';
const SomeClass = vi.fn();
SomeClass.prototype.someMethod = vi.fn();
vi.spyOn(mod, 'SomeClass').mockImplementation(SomeClass);
関数から返されたオブジェクトをスパイする
- キャッシュを使用した例:
// 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(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();
モジュールの一部をモックする
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
の時間も変更されることに注意してください。
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
をモックする
- 環境変数を変更するには、新しい値を割り当てるだけです。
WARNING
環境変数の値は、異なるテスト間で自動的にリセットされません。
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');
});
- 値を自動的にリセットしたい場合は、
unstubEnvs
設定オプションを有効にして(またはbeforeEach
フックでvi.unstubAllEnvs
を手動で呼び出して)、vi.stubEnv
ヘルパーを使用できます。
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');
});
// vitest.config.ts
export default defineConfig({
test: {
unstubEnvs: true,
},
});