モック
テストを作成する際には、内部または外部サービスの「偽の」バージョンを作成する必要があるのは時間の問題です。これは一般的にモックと呼ばれています。Vitestは、そのviヘルパーを通して、これを支援するためのユーティリティ関数を提供します。import { vi } from 'vitest'
でインポートするか、グローバルにアクセスできます(グローバル設定が有効になっている場合)。
警告
実行間でモックの状態変更を元に戻すために、常に各テスト実行の前または後にモックをクリアまたは復元することを忘れないでください!詳細については、mockReset
のドキュメントを参照してください。
すぐに始めたい場合は、APIセクションを確認してください。そうでない場合は、読み進めてモックの世界を深く掘り下げていきましょう。
日付
テスト時に一貫性を確保するために、日付を制御する必要がある場合があります。Vitestはタイマーとシステム日付を操作するために@sinonjs/fake-timers
パッケージを使用します。具体的なAPIの詳細についてはこちらを参照してください。
例
import { , , , , , } from 'vitest'
const = [9, 17]
function () {
const = new ().()
const [, ] =
if ( > && < )
return { : 'Success' }
return { : 'Error' }
}
('purchasing flow', () => {
(() => {
// tell vitest we use mocked time
.()
})
(() => {
// restoring date after each test run
.()
})
('allows purchases within business hours', () => {
// set hour within business hours
const = new (2000, 1, 1, 13)
.()
// access Date.now() will result in the date set above
(()).({ : 'Success' })
})
('disallows purchases outside of business hours', () => {
// set hour outside business hours
const = new (2000, 1, 1, 19)
.()
// access Date.now() will result in the date set above
(()).({ : 'Error' })
})
})
関数
関数のモックは、スパイとモックの2つの異なるカテゴリに分類できます。
特定の関数が呼び出されたかどうか(および可能性のある引数が渡されたかどうか)を確認する必要がある場合のみ、スパイを使用すれば十分です。これには、vi.spyOn()
(詳細はこちら)を直接使用できます。
ただし、スパイは関数についてスパイするのに役立つだけで、それらの関数の実装を変更することはできません。関数の偽の(またはモックされた)バージョンを作成する必要がある場合は、vi.fn()
(詳細はこちら)を使用できます。
関数のモックにはTinyspyをベースとして使用していますが、jest
と互換性を持たせるための独自のラッパーがあります。vi.fn()
とvi.spyOn()
は同じメソッドを共有しますが、vi.fn()
の戻り値のみが呼び出し可能です。
例
import { , , , , } from 'vitest'
function ( = .. - 1) {
return .[]
}
const = {
: [
{ : 'Simple test message', : 'Testman' },
// ...
],
, // can also be a `getter or setter if supported`
}
('reading messages', () => {
(() => {
.()
})
('should get the latest message with a spy', () => {
const = .(, 'getLatest')
(.()).('getLatest')
(.()).(
.[.. - 1],
)
().(1)
.(() => 'access-restricted')
(.()).('access-restricted')
().(2)
})
('should get with a mock', () => {
const = .().()
(()).(.[.. - 1])
().(1)
.(() => 'access-restricted')
(()).('access-restricted')
().(2)
(()).(.[.. - 1])
().(3)
})
})
その他
グローバル
vi.stubGlobal
ヘルパーを使用して、jsdom
またはnode
に存在しないグローバル変数をモックできます。グローバル変数の値をglobalThis
オブジェクトに入れます。
import { } from 'vitest'
const = .(() => ({
: .(),
: .(),
: .(),
: .(),
}))
.('IntersectionObserver', )
// now you can access it as `IntersectionObserver` or `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番目のアプローチの利点は、動的に異なる仮想エントリポイントを作成できることです。複数の仮想モジュールを1つのファイルにリダイレクトした場合、それらのすべてが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'
// this will only affect "foo" outside of the original module
vi.spyOn(mod, 'foo')
vi.mock('./foobar.js', async (importOriginal) => {
return {
...await importOriginal<typeof import('./foobar.js')>(),
// this will only affect "foo" outside of the original module
foo: () => 'mocked'
}
})
foobar
メソッドに実装を直接提供することで、この動作を確認できます。
// foobar.test.js
import * as mod from './foobar.js'
vi.spyOn(mod, 'foo')
// exported foo references mocked method
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'
// 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.jsで実行されるため、ネットワークリクエストのモックは困難です。Web APIは使用できないため、ネットワーク動作を模倣する必要があります。Mock Service Workerを推奨します。これにより、REST
とGraphQL
の両方のネットワークリクエストをモックでき、フレームワークに依存しません。
Mock Service Worker(MSW)は、テストが行うリクエストをインターセプトすることによって機能します。そのため、アプリケーションコードを変更せずに使用できます。ブラウザ内では、Service Worker APIを使用します。Node.jsおよびVitestでは、@mswjs/interceptors
ライブラリを使用します。MSWの詳細については、導入をお読みください。
設定
セットアップファイルで以下のように使用できます。
import { , , } from 'vitest'
import { } from 'msw/node'
import { , , } from 'msw'
const = [
{
: 1,
: 1,
: 'first post title',
: 'first post body',
},
// ...
]
export const = [
.get('https://rest-endpoint.example/path/to/posts', () => {
return .json()
}),
]
const = [
.query('ListPosts', () => {
return .json(
{
: { },
},
)
}),
]
const = (..., ...)
// Start server before all tests
(() => .listen({ : 'error' }))
// Close server after all tests
(() => .close())
// Reset handlers after each test `important for test isolation`
(() => .resetHandlers())
onUnhandleRequest: 'error'
を使用してサーバーを設定すると、対応するリクエストハンドラーがないリクエストがあるたびにエラーがスローされます。
例
MSWを使用する完全な動作例があります。React Testing with MSW。
その他
MSWには他にも多くの機能があります。Cookieやクエリパラメータにアクセスしたり、モックエラーレスポンスを定義したり、他にも多くのことができます!MSWでできることのすべてを見るには、ドキュメントをお読みください。
タイマー
タイムアウトまたは間隔を含むコードをテストする場合、テストがタイムアウトするまで待つのではなく、「偽の」タイマーを使用してsetTimeout
とsetInterval
への呼び出しをモックすることでテストを高速化できます。
詳細なAPIの説明については、vi.useFakeTimers
APIセクションを参照してください。
例
import { , , , , , } from 'vitest'
function () {
(, 1000 * 60 * 60 * 2) // 2 hours
}
function () {
(, 1000 * 60) // 1 minute
}
const = .(() => .('executed'))
('delayed execution', () => {
(() => {
.()
})
(() => {
.()
})
('should execute the function', () => {
()
.()
().(1)
})
('should not execute the function', () => {
()
// advancing by 2ms won't trigger the func
.(2)
()..()
})
('should execute every minute', () => {
()
.()
().(1)
.()
().(2)
})
})
チートシート
情報
以下の例では、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
を使用した例
警告
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('./some-path.js', () => {
const SomeClass = vi.fn()
SomeClass.prototype.someMethod = vi.fn()
return { SomeClass }
})
// SomeClass.mock.instances will have 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 will have returned object
vi.spyOn
を使用した例
import * as exports from './some-path.js'
vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
// whatever suites you from first two examples
})
関数から返されたオブジェクトをスパイする
- キャッシュを使用した例
// 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(),
}
}
// now every time that useObject() is called it will
// return the same object reference
return _cache
}
return { useObject }
})
const obj = useObject()
// obj.method was called inside some-path
expect(obj.method).toHaveBeenCalled()
モジュールの部分をモックする
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() // has original behaviour
mocked() // is a spy function
現在の日付をモックする
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())
// reset mocked time
vi.useRealTimers()
グローバル変数をモックする
globalThis
に値を代入するか、vi.stubGlobal
ヘルパーを使用してグローバル変数を設定できます。vi.stubGlobal
を使用する場合、unstubGlobals
設定オプションを有効にするか、vi.unstubAllGlobals
をbeforeEach
フックで手動で呼び出さない限り、異なるテスト間で自動的にリセットされません。
vi.stubGlobal('__VERSION__', '1.0.0')
expect(__VERSION__).toBe('1.0.0')
import.meta.env
をモックする
- 環境変数を変更するには、新しい値を代入するだけです。
警告
環境変数の値は、異なるテスト間で自動的にリセットされません。
import { beforeEach, expect, it } from 'vitest'
// you can reset it in beforeEach hook manually
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.unstubAllEnvs
を手動でbeforeEach
フックで呼び出して)、vi.stubEnv
ヘルパーを使用できます。
import { expect, it, vi } from 'vitest'
// before running tests "VITE_ENV" is "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 {
test: {
unstubAllEnvs: true,
}
}