はじめに
単体テストを書く際、外部依存関係をモックすることは避けられない作業です。特にNext.jsやVue、Reactなどのモダンなフロントエンドフレームワークを使用したプロジェクトでは、コンポーネントやサービスの独立したテストを行うためにモックが必須となります。
Vitestは、Viteをベースにした高速なJavaScriptテストフレームワークで、Jestと互換性のあるAPIを提供しています。今回はVitestでのモックの使い方について、実際のコード例を交えながら詳しく解説します。
Vitestのモックの基本概念
Vitestでは、vi
オブジェクトを通じてモックとスパイの機能が提供されています。基本的なモックの種類には以下のようなものがあります:
- 関数モック (
vi.fn()
)
- モジュールモック (
vi.mock()
)
- スパイ (
vi.spyOn()
)
それぞれの使い方を見ていきましょう。
1. 関数モック (vi.fn()
)
vi.fn()
は最も基本的なモックで、任意の関数をモックするために使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import { vi, expect, test } from 'vitest'
test('基本的な関数モック', () => {
// モック関数の作成
const mockFn = vi.fn()
// モック関数の呼び出し
mockFn()
mockFn('引数あり')
// 検証
expect(mockFn).toHaveBeenCalled()
expect(mockFn).toHaveBeenCalledTimes(2)
expect(mockFn).toHaveBeenCalledWith('引数あり')
})
|
戻り値の設定
モック関数の戻り値を設定するには、いくつかの方法があります:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 単一の値を返す
const mockReturnValue = vi.fn().mockReturnValue('固定値')
// 一度だけ特定の値を返す
const mockReturnValueOnce = vi.fn()
.mockReturnValueOnce('一回目')
.mockReturnValueOnce('二回目')
.mockReturnValue('それ以降')
// 実装をカスタマイズする
const mockImplementation = vi.fn().mockImplementation((arg) => {
return `処理した結果: ${arg}`
})
|
2. モジュールモック (vi.mock()
)
vi.mock()
は、モジュール全体をモックするために使用します。これは特に外部ライブラリや他のファイルからインポートされる関数やクラスをモックする場合に便利です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import { vi, expect, test } from 'vitest'
// モジュールをモック
vi.mock('./myModule', () => {
return {
someFunction: vi.fn().mockReturnValue('モック値'),
someObject: {
method: vi.fn().mockReturnValue('モックメソッド')
}
}
})
// モックされたモジュールをインポート
import { someFunction, someObject } from './myModule'
test('モジュールモック', () => {
const result = someFunction()
expect(result).toBe('モック値')
const methodResult = someObject.method()
expect(methodResult).toBe('モックメソッド')
})
|
実際のモジュールの一部を使用する
実際のモジュールの一部のみをモックし、他の部分は実際の実装を使用したい場合:
1
2
3
4
5
6
7
8
|
vi.mock('./myModule', async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// 特定の関数だけをモック
someFunction: vi.fn().mockReturnValue('モック値')
}
})
|
3. スパイ (vi.spyOn()
)
vi.spyOn()
は、既存のオブジェクトのメソッドを監視(スパイ)するために使用します。単純に呼び出しを追跡するだけでなく、必要に応じて実装を置き換えることもできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
import { vi, expect, test } from 'vitest'
const user = {
getName: () => 'Original Name',
getAge: () => 25
}
test('スパイの基本', () => {
// メソッドをスパイする
const getSpy = vi.spyOn(user, 'getName')
// 元の実装が呼ばれる
const name = user.getName()
expect(name).toBe('Original Name')
expect(getSpy).toHaveBeenCalled()
})
test('スパイ + モック実装', () => {
// スパイしながら実装を置き換える
const getSpy = vi.spyOn(user, 'getName').mockReturnValue('Mocked Name')
const name = user.getName()
expect(name).toBe('Mocked Name')
expect(getSpy).toHaveBeenCalled()
})
|
非同期コードのモック
非同期関数のモックも同様に簡単に行えます:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// Promise 成功のモック
const successMock = vi.fn().mockResolvedValue({ data: 'success' })
// Promise 失敗のモック
const failureMock = vi.fn().mockRejectedValue(new Error('Failed'))
// 非同期関数のテスト
test('非同期関数のモック', async () => {
const result = await successMock()
expect(result).toEqual({ data: 'success' })
await expect(failureMock()).rejects.toThrow('Failed')
})
|
テスト間でのモックのリセット
テスト間の独立性を保つために、モックをリセットする方法がいくつか用意されています:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// 全てのモックをリセット
beforeEach(() => {
vi.resetAllMocks() // 呼び出し情報をリセットするが、実装はそのまま
})
// あるいは
beforeEach(() => {
vi.clearAllMocks() // 呼び出し情報のみをクリア
})
// 実装も含めて完全にリセット
beforeEach(() => {
vi.restoreAllMocks() // スパイの場合、元の実装に戻す
})
|
実践例:外部APIをモックするテスト
GoogleスプレッドシートAPIを使用する関数をテストする例を見てみましょう:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
// waitlist.ts
import { GoogleSpreadsheet } from 'google-spreadsheet'
import { JWT } from 'google-auth-library'
export async function submitWaitlistForm(formData) {
// バリデーション
if (!formData.name) {
return { error: '名前は必須です' }
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
return { error: '有効なメールアドレスを入力してください' }
}
if (!formData.teamSize) {
return { error: 'チームサイズは必須です' }
}
try {
// Google Sheets APIの認証
const serviceAccountAuth = new JWT({
email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
key: process.env.GOOGLE_PRIVATE_KEY,
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
})
const doc = new GoogleSpreadsheet(
process.env.GOOGLE_SHEET_ID,
serviceAccountAuth
)
await doc.loadInfo()
const sheet = doc.sheetsByIndex[0]
// データを追加
await sheet.addRow({
name: formData.name,
email: formData.email,
company: formData.company,
teamSize: formData.teamSize,
message: formData.message,
})
return { success: true }
} catch (error) {
console.error('Error submitting form:', error)
return { error: 'フォームの送信中にエラーが発生しました' }
}
}
|
このような関数をテストするときは、外部依存関係(GoogleスプレッドシートAPI)をモックする必要があります:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
|
// waitlist.test.ts
import { submitWaitlistForm } from './waitlist'
import { GoogleSpreadsheet } from 'google-spreadsheet'
import { JWT } from 'google-auth-library'
import { vi, expect, describe, it, beforeEach } from 'vitest'
// モジュールをモック
vi.mock('google-spreadsheet', () => {
return {
GoogleSpreadsheet: vi.fn().mockImplementation(() => {
return {
loadInfo: vi.fn().mockResolvedValue(undefined),
sheetsByIndex: [
{
addRow: vi.fn().mockResolvedValue(undefined),
},
],
}
}),
}
})
vi.mock('google-auth-library', () => {
return {
JWT: vi.fn().mockImplementation(() => {
return {}
}),
}
})
describe('submitWaitlistForm', () => {
// 各テスト前にモックをクリア
beforeEach(() => {
vi.clearAllMocks()
})
it('正常なリクエストを処理する', async () => {
const formData = {
name: 'テスト太郎',
email: '[email protected]',
company: 'テスト株式会社',
teamSize: '2-5',
message: 'テストメッセージ',
}
// モックの実装を直接参照できるように変数に保存
const mockAddRow = vi.fn().mockResolvedValue(undefined)
const mockLoadInfo = vi.fn().mockResolvedValue(undefined)
const mockInstance = {
loadInfo: mockLoadInfo,
sheetsByIndex: [
{
addRow: mockAddRow,
},
],
}
// GoogleSpreadsheetモックの実装を書き換え
GoogleSpreadsheet.mockImplementation(() => mockInstance)
const result = await submitWaitlistForm(formData)
// 結果の検証
expect(result).toEqual({ success: true })
// モックが正しく呼び出されたことを検証
expect(GoogleSpreadsheet).toHaveBeenCalled()
expect(mockLoadInfo).toHaveBeenCalled()
expect(mockAddRow).toHaveBeenCalledWith(
expect.objectContaining({
name: 'テスト太郎',
email: '[email protected]',
company: 'テスト株式会社',
teamSize: '2-5',
message: 'テストメッセージ',
})
)
})
it('名前が欠けている場合はエラーを返す', async () => {
const formData = {
name: '',
email: '[email protected]',
company: '',
teamSize: '2-5',
message: '',
}
const result = await submitWaitlistForm(formData)
expect(result).toEqual({ error: '名前は必須です' })
})
// 他のテストケース...
})
|
モックのよくある問題と解決策
問題1: 新しく作成したインスタンスのメソッドをテストできない
次のようなコードはエラーになります:
1
2
3
|
const mockJWT = new JWT()
const mockDoc = new GoogleSpreadsheet('', mockJWT)
expect(mockDoc.loadInfo).toHaveBeenCalled() // エラー!
|
解決策: テスト内で新しいインスタンスを作るのではなく、モック実装への参照を直接保持し、それをテストに使用します:
1
2
3
4
5
6
7
8
9
10
11
12
|
const mockLoadInfo = vi.fn().mockResolvedValue(undefined)
const mockInstance = {
loadInfo: mockLoadInfo,
// ...
}
GoogleSpreadsheet.mockImplementation(() => mockInstance)
// 実際の関数を呼び出し
await myFunction()
// 直接モック関数を検証
expect(mockLoadInfo).toHaveBeenCalled() // 成功!
|
問題2: パス名が一致しないと正しくモックされない
1
2
|
vi.mock('./myModule') // ここでのパスが...
import { something } from '../utils/myModule' // ここと一致しない
|
解決策: 常にインポートと同じパスを使用します:
1
2
|
vi.mock('../utils/myModule')
import { something } from '../utils/myModule'
|
問題3: hoistingの問題
vi.mock()
はファイルの先頭に自動的にホイストされるため、以下のコードは想定通りに動作しません:
1
2
3
4
5
6
|
const value = 'テスト'
vi.mock('./myModule', () => {
return {
someFunction: vi.fn().mockReturnValue(value) // valueはundefined
}
})
|
解決策: ファクトリ関数内でローカル変数を使用します:
1
2
3
4
5
6
7
|
const value = 'テスト'
vi.mock('./myModule', () => {
const localValue = 'テスト' // ここで再定義
return {
someFunction: vi.fn().mockReturnValue(localValue)
}
})
|
まとめ
Vitestは、Jestに似た使いやすいモックAPIを提供しつつ、Viteの高速なビルドシステムを活用した素晴らしいテストフレームワークです。
モックを使いこなすポイントをまとめると:
- 単純な関数モックには
vi.fn()
を使う
- モジュール全体をモックするには
vi.mock()
を使う
- 既存のオブジェクトを監視するには
vi.spyOn()
を使う
- テスト間の独立性を保つために
vi.clearAllMocks()
や vi.restoreAllMocks()
を使う
- モックしたオブジェクトの参照を保持し、直接それを検証する
これらの手法を組み合わせることで、複雑なコードでも効率的にテストを書くことができます。テストコードがきれいに保たれていると、リファクタリングやバグの検出も容易になり、プロジェクトの品質向上につながります。
Vitestでのテスト作成を楽しんでください!