Jest で非同期エラーをキャッチする正しい書き方

非同期処理のテストで「あれ?エラーが正しく検出されていない」という経験をお持ちの開発者の皆さん、こんにちは。Jest で非同期エラーを確実にキャッチする正しい書き方について、実際のコード例とよくあるエラーメッセージを交えながら詳しく解説いたします。
非同期処理のテストは、同期処理とは異なる独特の難しさがありますね。特に、エラーハンドリングが正しく動作しているかを確認するテストでは、期待した通りにエラーがキャッチされずに悩むことがよくあります。この記事を読み終える頃には、皆さんがより自信を持って非同期テストを書けるようになることをお約束します。
Jest での非同期処理テストの基本
非同期処理と Jest の関係性
Jest は、JavaScript と TypeScript のテストフレームワークとして広く使われており、非同期処理のテストにも対応しています。しかし、非同期処理の性質上、テストの実行タイミングを正しく制御する必要があります。
非同期処理のテストには、主に以下の 3 つの方法があります。
# | 手法 | 特徴 | 適用場面 |
---|---|---|---|
1 | async/await | 最も直感的で読みやすい | Promise ベースの処理 |
2 | Promise チェーン | 従来の Promise 記法 | レガシーコードとの互換性 |
3 | done() コールバック | コールバック形式 | コールバック関数のテスト |
これらの中でも、現代的な開発では async/await
が最も推奨されています。
Jest のテスト実行の仕組み
Jest は、テスト関数が完了するタイミングを以下のように判断します。
typescript// 同期テストの例
test('同期処理のテスト', () => {
// テスト処理
expect(1 + 1).toBe(2);
// 関数の最後まで実行されると、テスト完了
});
一方、非同期テストでは、テストの完了タイミングを明示的に指定する必要があります。
非同期エラーハンドリングでよくある問題
最もよくある間違い:エラーが無視される問題
多くの開発者が最初に陥る罠がこちらです。以下のコードを見てください。
typescript// ❌ 間違った書き方
test('エラーがキャッチされない例', () => {
fetchUserData('invalid-id').catch((error) => {
expect(error.message).toBe('User not found');
});
});
このテストを実行すると、以下のような問題が発生します。
scssPASS src/user.test.ts
✓ エラーがキャッチされない例 (5ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
一見すると成功しているように見えますが、実際には catch
ブロックが実行される前にテストが終了してしまっています。
実際のエラーメッセージ例
正しくエラーハンドリングを行わないと、以下のようなエラーメッセージが表示されることがあります。
javascriptUnhandledPromiseRejectionWarning: Error: User not found
at fetchUserData (/src/userService.js:15:11)
at Object.<anonymous> (/src/user.test.js:8:5)
このエラーは、Promise の reject
が適切にハンドリングされていないことを示しています。
非同期処理でよく見るエラーパターン
開発現場でよく遭遇するエラーパターンをまとめました。
# | エラーパターン | 原因 | 対処法 |
---|---|---|---|
1 | UnhandledPromiseRejectionWarning | Promise の reject が処理されていない | await または return を使用 |
2 | Timeout エラー | テストが完了しない | 適切な非同期処理の書き方 |
3 | AssertionError が出ない | テストが実行されていない | expect.assertions() を使用 |
async/await を使った正しいエラーキャッチ
基本的な書き方
async/await
を使った正しいエラーキャッチの方法をご紹介します。
typescript// ✅ 正しい書き方
test('async/await でエラーをキャッチ', async () => {
await expect(fetchUserData('invalid-id')).rejects.toThrow(
'User not found'
);
});
この書き方では、expect().rejects.toThrow()
を使用することで、Promise が reject されることを期待していることを明示します。
try-catch 文を使った詳細なエラー検証
より詳細なエラー検証を行いたい場合は、try-catch
文を使用できます。
typescripttest('try-catch でエラーの詳細を検証', async () => {
expect.assertions(2); // 2つのアサーションが実行されることを保証
try {
await fetchUserData('invalid-id');
// エラーが発生しなかった場合はテスト失敗
expect(true).toBe(false);
} catch (error) {
expect(error.message).toBe('User not found');
expect(error.status).toBe(404);
}
});
expect.assertions(2)
は、テスト内で確実に 2 つのアサーションが実行されることを保証します。これにより、catch
ブロックが実行されなかった場合にテストが失敗するようになります。
カスタムエラークラスの検証
独自のエラークラスを使用している場合の検証方法です。
typescript// カスタムエラークラス
class UserNotFoundError extends Error {
constructor(message: string, public userId: string) {
super(message);
this.name = 'UserNotFoundError';
}
}
// テストコード
test('カスタムエラーの検証', async () => {
await expect(fetchUserData('invalid-id')).rejects.toThrow(
UserNotFoundError
);
});
Promise チェーンでのエラー処理
従来の Promise 記法でのエラーキャッチ
レガシーコードや特定の要件で Promise チェーンを使用する場合の正しい書き方です。
typescripttest('Promise チェーンでのエラーキャッチ', () => {
expect.assertions(1);
return fetchUserData('invalid-id')
.then(() => {
// エラーが発生しなかった場合はテスト失敗
expect(true).toBe(false);
})
.catch((error) => {
expect(error.message).toBe('User not found');
});
});
重要なポイントは、Promise を return
することです。これにより、Jest が Promise の完了を待つことができます。
resolves と rejects マッチャーの活用
Jest では、Promise の結果を直接検証できる便利なマッチャーが用意されています。
typescript// 成功ケースの検証
test('Promise が成功する場合', () => {
return expect(fetchUserData('valid-id')).resolves.toEqual(
{ id: 'valid-id', name: 'John Doe' }
);
});
// エラーケースの検証
test('Promise が失敗する場合', () => {
return expect(
fetchUserData('invalid-id')
).rejects.toThrow('User not found');
});
具体的な実装例とコード解説
実際の API サービスクラスのテスト
実際の開発現場でよく使われるパターンを見てみましょう。
typescript// テスト対象のサービスクラス
class UserService {
async getUserById(id: string): Promise<User> {
if (!id) {
throw new Error('User ID is required');
}
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error('User not found');
}
throw new Error('Failed to fetch user');
}
return response.json();
}
}
このサービスクラスに対して、さまざまなエラーケースをテストする方法をご紹介します。
パラメータエラーのテスト
typescriptdescribe('UserService', () => {
let userService: UserService;
beforeEach(() => {
userService = new UserService();
});
test('空のIDでエラーが発生することを確認', async () => {
await expect(
userService.getUserById('')
).rejects.toThrow('User ID is required');
});
test('nullのIDでエラーが発生することを確認', async () => {
await expect(
userService.getUserById(null as any)
).rejects.toThrow('User ID is required');
});
});
ネットワークエラーのテスト
ネットワークエラーをシミュレートする場合は、fetch
をモックします。
typescript// fetch をモック
global.fetch = jest.fn();
test('404エラーの処理', async () => {
// fetch が404を返すようにモック
(
fetch as jest.MockedFunction<typeof fetch>
).mockResolvedValueOnce({
ok: false,
status: 404,
} as Response);
await expect(
userService.getUserById('not-found')
).rejects.toThrow('User not found');
});
複数のエラー条件を表形式でテスト
多数のエラーケースを効率的にテストするために、テーブル駆動テストを活用できます。
typescripttest.each([
['', 'User ID is required'],
[null, 'User ID is required'],
[undefined, 'User ID is required'],
])(
'不正なID "%s" でエラーが発生する',
async (invalidId, expectedError) => {
await expect(
userService.getUserById(invalidId as string)
).rejects.toThrow(expectedError);
}
);
非同期処理のタイムアウトテスト
長時間かかる処理のタイムアウトをテストする場合の書き方です。
typescripttest('タイムアウトエラーのテスト', async () => {
// 3秒でタイムアウトするよう設定
jest.setTimeout(3000);
// 長時間かかる処理をモック
(
fetch as jest.MockedFunction<typeof fetch>
).mockImplementationOnce(
() =>
new Promise((resolve) => setTimeout(resolve, 5000))
);
await expect(
userService.getUserById('slow-id')
).rejects.toThrow('timeout');
}, 4000); // テスト自体のタイムアウトを4秒に設定
エラーメッセージの部分マッチング
エラーメッセージの一部分だけを検証したい場合の方法です。
typescripttest('エラーメッセージの部分マッチング', async () => {
await expect(
userService.getUserById('invalid')
).rejects.toThrow(/User.*not found/);
});
まとめ
Jest での非同期エラーハンドリングについて、実践的な書き方をご紹介してまいりました。
重要なポイントのおさらい:
async/await
とexpect().rejects.toThrow()
の組み合わせが最も確実で読みやすい書き方ですexpect.assertions()
を使用することで、テストが確実に実行されることを保証できますreturn
文を忘れずに記述することで、Promise の完了を Jest に伝えることができます- 実際のエラーメッセージを含めることで、より具体的で検索しやすいテストが書けます
非同期処理のテストは最初は難しく感じるかもしれませんが、パターンを覚えてしまえば確実にテストを書くことができるようになります。何より、エラーハンドリングが正しく動作することを確認できるテストがあることで、開発チーム全体の安心感が大きく向上しますね。
今回ご紹介した手法を活用して、より堅牢で信頼性の高いアプリケーションを開発していただければと思います。皆さんの開発がより楽しく、そして効率的になることを心から願っております。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来