Vitestでクラスベースライブラリのモックに躓いた背景
画像ダウンロード機能のユニットテストを作成中に、JSZipライブラリのモックでテストが失敗する問題に遭遇しました。
正直なところ、このエラー原因の特定に予想以上に時間がかかりましたが、解決できたおかげでテスト実行が安定して通りやすくなりました。
本記事では、実際の開発現場で遭遇した「not a constructor」エラーの具体的な解決策をコード例と共に解説します。
こんな人におすすめ
- Vitestでクラスベースライブラリをモックしたい方
- 「not a constructor」エラーで困っている開発者
- JSZipのテストを書いているエンジニア
newキーワードで呼び出すライブラリのモック方法を知りたい方
目次
- Vitestでクラスベースライブラリのモックに躓いた背景
- 「not a constructor」エラーの症状
- Vitestでクラスベースライブラリのモックが失敗する原因
- 【解決策】Vitestでクラスを正しくモックする方法
- モック方法の選択ガイド
- クラスベースのモック導入で得られた効果
- Vitestでクラスモックする際のポイントまとめ
- 確認手順
「not a constructor」エラーの症状
テスト実行時に、JSZipのインスタンス化でエラーが発生しました。
TypeError: () => mockZip is not a constructor
このエラーメッセージからは、モックの定義に問題があることが読み取れませんでした。
実際にデバッグに時間を要しましたが、原因はモックの定義方法にありました。
Vitestでクラスベースライブラリのモックが失敗する原因
JSZipは new JSZip() でインスタンス化するクラスベースのライブラリです。
Vitestで vi.fn(() => mockZip) を使うと、関数として呼び出されることを期待してしまい、new キーワードでのコンストラクタ呼び出しに対応できません。
NG: 関数ベースのモック(失敗例)
vi.mock('jszip', () => {
const mockZip = {
file: vi.fn(),
generateAsync: vi.fn().mockResolvedValue(new Blob(['mock'])),
};
return { default: vi.fn(() => mockZip) };
});
このモック定義では、vi.fn() で作成された関数を返しているため、new キーワードでの呼び出し時に「not a constructor」エラーが発生します。
なぜ失敗するのか: vi.fn() は関数オブジェクトを作成しますが、これは new キーワードでコンストラクタとして呼び出すことはできません。
以下のシーケンス図で、関数ベースのモックが失敗する流れを確認できます。
sequenceDiagram
participant Test as テストコード
participant Vitest as vi.mock()
participant Mock as vi.fn()(関数)
participant Error as TypeError
Test->>Vitest: vi.mock('jszip', ...) を定義
Vitest->>Mock: vi.fn(() => mockZip) を返す
Note over Mock: 関数オブジェクト(コンストラクタではない)
Test->>Mock: new JSZip() を呼び出し
Mock-->>Error: ❌ "not a constructor"
Note over Error: new キーワードで<br/>関数をインスタンス化できない
【解決策】Vitestでクラスを正しくモックする方法
モックをクラス定義に変更します。
OK: クラスベースのモック(正解例)
vi.mock('jszip', () => {
return {
default: class MockJSZip {
file = vi.fn();
generateAsync = vi.fn().mockResolvedValue(new Blob(['mock zip content']));
},
};
});
このモック定義では、クラス構文を使ってモックを定義しているため、new キーワードでの呼び出しに正しく対応できます。
ポイント:
class構文を使ってモックを定義する- クラスプロパティとして
vi.fn()を設定する defaultエクスポートにクラスを返す
以下のシーケンス図で、クラスベースのモックが成功する流れを確認できます。
sequenceDiagram
participant Test as テストコード
participant Vitest as vi.mock()
participant Mock as class MockJSZip
participant Methods as モックメソッド
Test->>Vitest: vi.mock('jszip', ...) を定義
Vitest->>Mock: class MockJSZip を返す
Note over Mock: クラス定義(コンストラクタとして機能)
Test->>Mock: new JSZip() を呼び出し
Mock-->>Test: ✅ インスタンスを返す
Test->>Methods: zip.file() を呼び出し
Methods-->>Test: モックされた振る舞いを返す
Test->>Methods: zip.generateAsync() を呼び出し
Methods-->>Test: mockResolvedValue を返す
Note over Methods: クラスプロパティとして<br/>vi.fn() が設定されている
Vitestクラスモックの基本パターン
クラスベースのライブラリをモックする際の基本パターンは以下の通りです。
vi.mock('ライブラリ名', () => ({
default: class MockClassName {
メソッド名 = vi.fn();
プロパティ名 = 値;
},
}));
このパターンはJSZip以外にも、以下のようなクラスベースライブラリで活用できます。
- ファイル操作系ライブラリ(FileSaver.jsなど)
- 画像処理系ライブラリ
- APIクライアント系ライブラリ
モック方法の選択ガイド
ライブラリの特性に応じて、適切なモック方法を選択するフローチャートです。
flowchart TD
A[ライブラリをモックする] --> B{new キーワードで<br/>呼び出すか?}
B -->|はい| C[クラスベースのモック]
B -->|いいえ| D[関数ベースのモック]
C --> E{default exportか?}
E -->|はい| F["return: default = class MockClass"]
E -->|いいえ| G["return: ClassName = class MockClass"]
D --> H{default exportか?}
H -->|はい| I["vi.fn または<br/>vi.fnでreturnValueを返す"]
H -->|いいえ| J["methodName: vi.fnを設定"]
F --> K[✅ new ライブラリ名 で呼び出し可能]
G --> K
I --> L[✅ 通常の関数呼び出しで使用]
J --> L
style C fill:#90EE90
style D fill:#87CEEB
style K fill:#FFD700
style L fill:#FFD700
使い方:
- ライブラリのドキュメントで
newキーワードの使用を確認 - 上記フローチャートに従ってモック方法を選択
- 適切な
vi.mock()パターンを適用
クラスベースのモック導入で得られた効果
クラスベースのモックに変更後、以下の効果が得られました。
- テスト実行が安定して通りやすくなりました
- テストコードが実際のJSZipの使用方法に近くなり、可読性が向上しました
- 他のクラスベースライブラリ(例:ファイル操作系ライブラリ)のモックにも同様のパターンを適用できるようになりました
正しいモック方法のおかげで、テスト実行時間が短縮され、デバッグ効率が向上しました。
Vitestでクラスモックする際のポイントまとめ
クラスベースライブラリをモックする際は、以下の点に注意が必要です。
- モックもクラスとして定義する必要があります -
vi.fn()ではなくclass構文を使用 newキーワードで呼び出されるライブラリは、関数ではなくクラス構文でモックを書きます- VitestのES Module モックでは
defaultエクスポートに注意します - 名前付きエクスポートの場合は適切に調整
関連するVitestのトラブルシューティング
Vitestでのテスト中に他の問題が発生した場合は、以下の記事も参考にしてください。
- Vitest fake timersとReact Testing Library waitForの相性問題と解決策 -
waitForがタイムアウトする問題の解決方法
確認手順
npm run test -- tests/utils/downloadImage.test.tsでテストを実行します- 全テストがパスすることを確認します


