TypeScriptで非同期処理の単体テストを書く方法を徹底解説!JestでPromiseやasync/awaitをマスター
生徒
「TypeScriptで非同期処理、つまり待ち時間が発生する処理のテストを書きたいのですが、どうすればいいですか?」
先生
「非同期処理のテストは、Jestというツールを使うと、Promiseやasync/awaitの状態を簡単にチェックできるんですよ。」
生徒
「普通のテストと何が違うんでしょうか?具体的な書き方を教えてほしいです!」
先生
「完了を待たずにテストが終わってしまう失敗を防ぐコツがあります。基本から順番に見ていきましょう!」
1. 非同期処理と単体テストの基本
プログラミングの世界には、処理が終わるまで時間がかかる作業があります。これを非同期処理と呼びます。例えば、インターネットからデータをダウンロードしたり、大きなファイルを読み込んだりする作業です。これらは、指示を出してから結果が返ってくるまでに「待ち時間」が発生します。
TypeScriptでは、この待ち時間を扱うためにPromise(プロミス)やasync/await(エイシンク・アウェイト)という仕組みを使います。Promiseは「将来、結果を返す約束」のようなものです。async/awaitは、その約束をより直感的に、まるで普通の順番通りの処理のように書ける魔法の言葉です。
単体テストとは、プログラムの最小単位(関数など)が正しく動いているかを確認する作業です。非同期処理のテストが難しいのは、テストプログラムが「結果を待たずに」勝手に終了してしまうことがあるからです。そのため、テストツールに対して「この処理が終わるまで待ってね」と正しく伝える必要があります。
2. テストツールJestの準備
TypeScriptでテストを書く際に最も人気があるのがJest(ジェスト)というフレームワークです。これを使うと、「この計算結果は10になるはずだ」といった期待値を簡単に記述できます。まずは、テスト環境が整っていることを前提に進めますが、非同期テストを書く上で大切なのは、テスト関数自体を非同期対応にすることです。
通常のテストは、関数の内容を上から下へ実行して終わりですが、非同期テストでは関数の前にasyncを付けることで、その中でawaitを使えるようになります。これにより、非同期処理の完了を待機してから、結果を検証(アサーション)できるようになります。アサーションとは、プログラムの実行結果が予想通りかどうかを判定することです。
3. async/awaitを使った基本的なテスト
まずは、最も一般的で分かりやすいasync/awaitを使ったテスト方法を見てみましょう。この方法は、コードが読みやすく、初心者の方に一番おすすめの書き方です。以下の例では、数秒後に挨拶を返す簡単な関数をテストします。
// テスト対象の関数(2秒後にメッセージを返す)
export const fetchGreeting = async (name: string): Promise<string> => {
return `こんにちは、${name}さん`;
};
// テストコード
test("非同期で挨拶が返ってくることを確認する", async () => {
const result = await fetchGreeting("田中");
expect(result).toBe("こんにちは、田中さん");
});
このコードでは、test関数の第2引数にasyncを付けています。そして、fetchGreetingを呼び出す際にawaitを付けることで、結果が返ってくるまでテストを一時停止します。結果が返ってきたら、expectを使って内容をチェックします。もしawaitを忘れると、結果を受け取る前にテストが終わってしまい、正しく検証できません。
4. Promiseのresolveをテストする
次に、async/awaitを使わずに、Promiseの機能そのものを使ってテストする方法を紹介します。Jestにはresolvesという便利な機能が備わっています。これは「Promiseが成功(解決)することを確認する」という命令です。
// テスト対象の関数(数値を2倍にして返すPromise)
export const doubleAsync = (num: number): Promise<number> => {
return Promise.resolve(num * 2);
};
// テストコード
test("Promiseが解決して数値が2倍になることを確認する", () => {
return expect(doubleAsync(5)).resolves.toBe(10);
});
ここで重要なのは、returnを忘れないことです。returnを書くことで、Jestに対して「このPromiseが解決するまで待ってから終了してね」と伝えています。resolvesを使うと、コードが一行でスッキリと書けるのがメリットです。初心者の方は、まずawaitを使った書き方に慣れてから、こちらの書き方も覚えていくと良いでしょう。
5. エラー(例外)が発生する場合のテスト
プログラムは常に成功するとは限りません。通信エラーや入力ミスなどで、処理が失敗することもあります。非同期処理でエラーが発生することをReject(リジェクト)と言います。テストでは、「正しくエラーが発生すること」を確認するのも非常に重要です。
// テスト対象の関数(エラーを投げる非同期関数)
export const fetchError = async (): Promise<void> => {
throw new Error("データが見つかりません");
};
// テストコード
test("エラーが正しく投げられることを確認する", async () => {
await expect(fetchError()).rejects.toThrow("データが見つかりません");
});
この例ではrejectsを使っています。これは先ほどのresolvesの逆で、「Promiseが失敗すること」を期待する書き方です。toThrowを組み合わせることで、エラーメッセージの内容まで厳密にチェックできます。意図しないエラーでプログラムが止まらないよう、失敗パターンのテストもしっかり書いておきましょう。
6. 実際の実行結果を確認しよう
テストを書いたら、実際にコマンドを入力して実行します。通常はターミナルやコマンドプロンプトでnpm testのようなコマンドを打ちます。Jestが実行されると、以下のような結果が表示されます。
PASS ./sample.test.ts
i 非同期で挨拶が返ってくることを確認する (5ms)
i Promiseが解決して数値が2倍になることを確認する (2ms)
i エラーが正しく投げられることを確認する (10ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 1.2s
緑色の文字でPASSと表示されれば、あなたの書いたテストがすべて成功したことを意味します。もし赤色でFAILと出た場合は、プログラムのどこかに間違いがあるか、テストの期待値が間違っていることになります。エラーメッセージを読んで、少しずつ修正していきましょう。
7. 非同期テストでよくある間違いと対策
初心者の方が一番ハマりやすいポイントは、やはり「待機」の忘れです。非同期処理を含む関数をテストしているのに、awaitを書かなかったり、Promiseをreturnしなかったりすると、テストは一瞬で終了します。その場合、テストは成功したように見えても、実は中身が実行されていないという「偽の成功」を招くことがあります。
また、タイマー処理(setTimeoutなど)を使っている場合のテストには、Jestが提供する「偽のタイマー(Fake Timers)」という機能を使うこともあります。これは、実際の待ち時間をスキップして、すぐに時間を進めることができる強力なツールです。大規模な開発では必須の知識となりますが、まずは基本のasync/awaitを完璧にマスターすることから始めましょう。
8. より実践的な非同期テストの例
最後に、複数のデータを扱う少しだけ複雑な例を見てみましょう。リストの中に特定の名前が含まれているかを非同期で確認するテストです。実際の開発では、このように配列やオブジェクトを扱う場面が非常に多いです。
// テスト対象の関数(ユーザーリストを非同期で取得する)
export const getUsers = async (): Promise<string[]> => {
return ["佐藤", "鈴木", "高橋"];
};
// テストコード
test("ユーザーリストに鈴木さんが含まれているか確認", async () => {
const users = await getUsers();
expect(users).toContain("鈴木");
expect(users.length).toBe(3);
});
このテストでは、toContainを使って配列の中に特定の要素があるかを確認し、さらにlengthを使ってデータの件数もチェックしています。複数のアサーションを一つのテストの中に書くことで、より信頼性の高いプログラムを作ることができます。非同期処理のテストは、最初は難しく感じるかもしれませんが、パターンを覚えてしまえば非常に心強い味方になってくれます。