2023年1月31日

Prisma を使用したテストの究極ガイド: ユニットテスト

ユニットテストとは、コードの個々の独立したユニットをテストして、それらが期待どおりに動作することを確認することです。この記事では、ユニットテストを行うべきコードベースの領域を特定する方法、それらのテストを作成する方法、および Prisma Client を使用した関数に対するテストを処理する方法について学習します。

The Ultimate Guide to Testing with Prisma: Unit Testing

目次

はじめに

ユニットテストは、アプリケーション内のコードの個々のユニット (例: <強調>関数) が期待どおりに機能することを保証する主要な方法の 1 つです。

テストに慣れていない人にとって、ユニットテストとは何かを理解することは非常に難しい場合があります。アプリケーションの仕組み、テストの書き方、テスト環境の準備方法を理解する必要があるだけでなく、何をテストすべきかを理解する必要もあります!

そのため、開発者はしばしばこのテストへのアプローチを取ります。

: @RoxCodes の正直さに感謝 😉

このシリーズでは、完全に機能するアプリケーションを使用します。そのコードベースで唯一欠けているのは、意図したとおりに動作することを検証するためのテストスイートです。

このシリーズを通して、コードのさまざまな領域を検討し、何をテストすべきか、なぜテストする必要があるのか、そしてそれらのテストをどのように記述するかを順を追って説明します。これには、ユニットテスト統合テストエンドツーエンドテスト、およびそれらのテストを実行する継続的インテグレーション (CI) および 継続的デプロイメント (CD) ワークフローのセットアップが含まれます。

具体的にこの記事では、コードの特定の領域に焦点を当て、それらに対するユニットテストを作成して、それらの領域の個々の構成要素が適切に機能していることを確認します。

ユニットテストとは?

ユニットテストは、小さく独立したコード片に対してテストを作成するテストの一種です。ユニットテストは、さまざまな状況で期待どおりに動作することを保証するために、コードの小さなユニットを対象とします。

通常、ユニットテストは個々の function を対象とします。関数は通常、JavaScript アプリケーションで最小の単一コードユニットであるためです。

以下の関数を例として取り上げます。

この関数は単純ですが、ユニットテストの適切な候補です。単一の機能セットが 1 つの関数にラップされています。この関数が適切に動作することを保証するために、文字列 'abcde' を提供し、文字列 'edcba' が返されることを確認する場合があります。

関連するテストのスイート、またはテストのセットは、次のようになります。

上記でお気づきかもしれませんが、ユニットテストの目標は、アプリケーションの最小の構成要素が適切に機能することを保証することだけです。これを行うことで、これらの構成要素を組み合わせ始めると、結果の動作が予測可能になるという信頼を築きます。

Test graphic

これが非常に重要な理由は、上記で説明したとおりです。ユニットテストを実行すると、すべてのテストに合格した場合、すべての構成要素が機能し、その結果、アプリケーションが意図したとおりに動作することが保証されます。ただし、1 つでもテストが失敗した場合、アプリケーションが意図したとおりに動作していないと見なすことができ、失敗したテストに基づいて何が間違っているかを正確に知ることができます。

ユニットテストではないもの?

ユニットテストでは、カスタムコードが意図したとおりに動作することを確認することが目標です。前の文で注意すべき重要な点は、「カスタムコード」というフレーズです。

JavaScript 開発者として、npm を介して、コミュニティ構築のモジュールとパッケージの豊富なエコシステムにアクセスできます。外部ライブラリを使用すると、車輪の再発明に費やす可能性のある時間を大幅に節約できます。

外部モジュールを使用することに問題はありませんが、それらのモジュールを使用する関数をテストすることを考える際に考慮すべき点がいくつかあります。最も重要なことは、これを念頭に置いておくことです。

外部パッケージを信頼しておらず、それに対してテストを作成する必要があると感じる場合は、おそらくその特定のパッケージを使用すべきではありません。

次の関数を例として取り上げます。

この関数は、正方形の 1 辺の長さを入力として受け取り、正方形の一意の色を含む、より詳細な正方形を含むオブジェクトを返します。

上記の関数のユニットテストを作成する際に、次のことを検証する必要がある場合があります。

  • 1 より小さい数値が指定された場合、関数は null を返します
  • 関数は面積を適切に計算します
  • 関数は、正しい値を持つ正しい形状のオブジェクトを返します
  • randomColor 関数が 1 回呼び出されました

各正方形が実際には一意の色を取得することを確認するためのテストについては言及されていないことに注意してください。これは、randomColor が外部モジュールであるため、適切に動作すると想定されているためです。

: randomColor が npm パッケージを介して提供されたか、別のファイルのカスタムビルド関数であっても、このコンテキストでは正しく動作すると想定する必要があります。randomColor が別のファイルで作成した関数である場合は、独自の独立したコンテキストでテストする必要があります。「構成要素」を考えてください!

この概念は、Prisma Client にも適用されるため重要です。アプリケーションで Prisma を使用する場合、Prisma Client は外部モジュールです。このため、すべてのテストでは、クライアントによって提供される関数が期待どおりに動作することを想定する必要があります。

使用するテクノロジー

前提条件

前提知識

このシリーズを開始するにあたり、次の知識があると役立ちます。

  • JavaScript または TypeScript の基本的な知識
  • Prisma Client とその機能の基本的な知識
  • Express の経験があると良い

開発環境

提供される例に従うには、以下が必要です。

  • Node.js がインストールされている
  • お好みのコードエディター ( VSCode を推奨)
  • Git がインストールされている

このシリーズでは、この GitHub リポジトリ を多用します。リポジトリをクローンし、main ブランチをチェックアウトしてください。

リポジトリをクローンする

ターミナルで、プロジェクトを保存するディレクトリに移動します。そのディレクトリで、次のコマンドを実行します。

上記のコマンドは、プロジェクトを express_sample_app という名前のフォルダーにクローンします。そのリポジトリのデフォルトブランチは main であるため、この時点で準備完了です!

リポジトリをクローンしたら、プロジェクトをセットアップするためにいくつかの手順を実行する必要があります。

まず、プロジェクトに移動し、node_modules をインストールします。

次に、プロジェクトのルートに .env ファイルを作成します。

このファイルには、API_SECRET という名前の変数が含まれている必要があります。この変数の値は任意の string に設定できます。また、DATABASE_URL という名前の変数も含まれている必要があります。これは今のところ空のままにすることができます。

.env では、API_SECRET 変数は、認証サービスがパスワードを暗号化するために使用する秘密鍵を提供します。実際のアプリケーションでは、この値は数値とアルファベット文字を含む長いランダム文字列に置き換える必要があります。

DATABASE_URL は、名前が示すように、データベースへの URL が含まれています。現在、実際のデータベースは必要ありません。

最後に、Prisma スキーマに基づいて Prisma Client を生成する必要があります。

API の探索

ユニットテストとは何か、ユニットテストではないものについて一般的な考え方ができたので、このシリーズでテストするアプリケーションを見てみましょう。

Github からクローンしたプロジェクトには、完全に機能する Express API が含まれています。この API を使用すると、ユーザーはログインして、お気に入りの引用を保存および整理できます。

アプリケーションのファイルは、src ディレクトリ内のフォルダーに機能別に整理されています。

src 内には、3 つのメインフォルダーがあります。

  • /auth: API の認証に直接関連するすべてのファイルが含まれています
  • /quotes: API の引用機能に直接関連するすべてのファイルが含まれています
  • /lib: 一般的なヘルパーファイルが含まれています

API 自体は、次のエンドポイントを提供します。

エンドポイント説明
POST /auth/signupユーザー名とパスワードを使用して新しいユーザーを作成します。
POST /auth/signinユーザー名とパスワードを使用してユーザーをログインします。
GET /quotesログイン中のユーザーに関連するすべての引用を返します。
POST /quotesログイン中のユーザーに関連する新しい引用を保存します。
DELETE /quotes/:idログイン中のユーザーに属する引用を ID で削除します。

時間をかけてこのプロジェクトのファイルを探索し、API の仕組みを感じてください。

ユニットテストとは何か、アプリケーションがどのように動作するかを一般的に理解できたので、アプリケーションが意図したとおりに動作することを検証するためのテストの作成プロセスを開始する準備ができました。

: 実際の設定では、これらのテストは、アプリケーションが進化および変更しても、既存の機能がそのまま維持されるようにするのに役立ちます。テストは、アプリケーションが完了した後ではなく、アプリケーションを開発するときに作成される可能性があります。

Vitest のセットアップ

テストを開始するには、テストフレームワークをセットアップする必要があります。このシリーズでは、Vitest を使用しています。

vitestvitest-mock-extended を次のコマンドでインストールすることから始めます。

: 上記のインストールされた 2 つのパッケージに関する情報については、このシリーズの最初の記事を必ずお読みください。

次に、Vitest がユニットテストの場所と、それらのテストにインポートする必要があるモジュールを解決する方法を認識するように Vitest を構成する必要があります。

vitest.config.unit.ts という名前の新しいファイルをプロジェクトのルートに作成します。

このファイルは、Vitest によって提供される defineConfig 関数を使用して、ユニットテストの構成を定義およびエクスポートします。

上記では、Vitest の 2 つのオプションを構成しました。

  • test.include オプションは、src ディレクトリ内の *.test.ts という命名規則に一致するすべてのファイル内でテストを探すように Vitest に指示します。
  • resolve.alias 構成は、ファイルパスエイリアスを設定します。これにより、ファイルインポートパスを短縮できます。例: src/auth/auth.serviceauth/auth.service になります。

最後に、テストをより簡単に実行するために、package.json にスクリプトを構成して、Vitest CLI コマンドを実行します。

package.jsonscripts セクションに以下を追加します。

上記で 2 つの新しいスクリプトが追加されました。

  • test:unit: これは、上記で作成した構成ファイルを使用して vitest CLI コマンドを実行します。
  • test:unit:ui: これは、上記で作成した構成ファイルを使用してui モードvitest CLI コマンドを実行します。これにより、ブラウザーで GUI が開き、テストの結果を検索、フィルター処理、および表示するためのツールが提供されます。

これらのコマンドを実行するには、プロジェクトのルートでターミナルで次のコマンドを実行します。

: 今すぐこれらのコマンドのいずれかを実行すると、コマンドが失敗することがわかります。それは、実行するテストがないためです!

この時点で、Vitest が構成され、ユニットテストの作成について考え始める準備ができました。

テストする必要のないファイル

すぐにテストの作成に取り掛かる前に、まずテストする必要のないファイルを見て、その理由を考えてみましょう。

以下は、テストする必要のないファイルのリストです。

  • src/index.ts
  • src/auth/auth.router.ts
  • src/auth/auth.schemas.ts
  • src/quotes/quotes.router.ts
  • src/quotes/quotes.schemas.ts
  • src/quotes/quotes.service.ts
  • src/lib/prisma.ts
  • src/lib/createServer.ts

これらのファイルには、ユニットテストが必要なカスタム動作はありません。

次の 2 つのセクションでは、これらのファイルがテストを必要としない 2 つの主要なシナリオについて説明します。

ファイルにカスタム動作がない

アプリケーションの次の例を見てください。

src/quotes/quotes.router.ts では、実際に発生しているのは、Express フレームワークによって提供される関数の呼び出しだけです。いくつかのカスタム関数 (validate および QuoteController.*) が使用されていますが、これらは別のファイルで定義されており、独自のコンテキストでテストされます。

2 番目のファイル src/auth/auth.schemas.ts も非常によく似ています。このファイルはアプリケーションにとって重要ですが、テストするものは実際には何もありません。コードは、外部モジュール zod を使用して定義されたスキーマをエクスポートするだけです。

関数が外部モジュールのみを呼び出す

指摘しておくべきもう 1 つのシナリオは、src/quotes/quotes.service.ts のシナリオです。

このサービスは、2 つの関数をエクスポートします。どちらの関数も、Prisma Client 関数の呼び出しをラップし、結果を返します。

この記事で前に述べたように、外部コードをテストする必要はありません。そのため、このファイルはスキップできます。

テストを必要としない上記のリストの残りのファイルを見ると、ここで概説した理由のいずれかの理由でテストが必要ないことがわかります。

テスト対象

プロジェクトの残りの .ts ファイルにはすべて、ユニットテストが必要な機能が含まれています。テストが必要なファイルの完全なリストは次のとおりです。

  • src/auth/auth.controller.ts
  • src/auth/auth.service.ts
  • src/lib/middlewares.ts
  • src/lib/utility-classes.ts
  • src/quotes/quotes.controller.ts
  • src/quotes/tags.service.ts

これらの各ファイルの各関数には、正しく動作することを検証する独自のテストスイートが必要です。

想像できるように、これにより多くのテストが発生する可能性があります!これを数値にすると、Express API にはテストが必要な 13 個の異なる関数が含まれており、それぞれに 2 つ以上のテストのスイートが含まれる可能性があります。これは、少なくとも 26 個のテストを作成することになることを意味します!

この記事を管理しやすい長さに保つために、この記事で取り上げたいすべての重要なユニットテストの概念を網羅しているため、単一のファイル src/quotes/tags.service.ts のテストを作成します。

: この API のテストの完全なセットがどのようなものになるか興味がある場合は、Github リポジトリの unit-tests ブランチに、すべての関数の完全なテストセットがあります。

タグサービスのテスト

タグサービスは、upsertTagsdeleteOrphanedTags の 2 つの関数をエクスポートします。

まず、tags.service.ts と同じディレクトリに、tags.service.test.ts という名前の新しいファイルを作成します。

: テストを整理する方法はたくさんあります。このシリーズでは、テストはテスト対象のすぐ隣のファイルに記述されます。これは、テストを併置するとも呼ばれます。

VSCode を使用しており、v1.64 以降をお持ちの場合は、テストとそのターゲットを併置するときにプロジェクトのファイルツリーをクリーンアップするクールな機能にアクセスできます。

VSCode 内で、画面上部のオプションバーの コード > プリファレンス > 設定 に移動します。

設定ページ内で、file nesting と入力してファイルネスト設定を検索します。以下の設定を有効にします。

File nesting option in VSCode

次に、これらの設定を少し下にスクロールすると、エクスプローラー > ファイルネスト: パターン セクションが表示されます。

*.ts という名前の項目が存在しない場合は、作成します。次に、*.ts 項目の値を ${capture}.*.ts に更新します。

File nesting setting in VSCode

これにより、VSCode は ${capture}.ts という名前のメインファイルの下に任意のファイルをネストできます。よりわかりやすくするために、次の例を参照してください。

Nested files

上記では、quotes.controller.ts という名前のファイルを確認できます。そのファイルの下にネストされているのは quotes.controller.test.ts です。厳密には必要ありませんが、この設定は、ユニットテストを併置するときにファイルツリーを少しクリーンアップするのに役立つ場合があります。

必要なモジュールをインポートする

新しい tags.service.test.ts ファイルの先頭に、テストを作成できるようにするために必要なものをいくつかインポートする必要があります。

以下は、これらの各インポートの使用目的です。

  • TagsService: これは、テストを作成しているサービスです。その関数を呼び出すことができるように、インポートする必要があります。
  • prismaMock: これは、lib/__mocks__/prisma で提供される Prisma Client のモックバージョンです。
  • randomColor: upsertTags 関数内でランダムな色を生成するために使用されるライブラリ。
  • describe: テストスイートを記述できるようにする vitest によって提供される関数。

注意すべき重要な点は、prismaMock インポートです。これは、データベースに実際にアクセスせずに prisma クエリを実行できるモックされた Prisma Client インスタンスです。モックされているため、クエリ応答を操作したり、そのメソッドをスパイしたりすることもできます。

: prismaMock インポートとは何か、およびその仕組みが不明な場合は、このモジュールの役割が説明されているこのシリーズの前の記事を必ずお読みください。

テストスイートの説明

Vitest によって提供される describe 関数を使用して、この特定のテストセットを記述できるようになりました。

これにより、テスト結果を出力するときに、このファイル内のテストが 1 つのセクションにグループ化され、どのスイートが合格し、失敗したかを確認しやすくなります。

ターゲットファイルで使用されるモジュールをモックする

実際のテストスイートを作成する前に行う最後の手順は、tags.service.ts ファイル内で使用される外部モジュールをモックすることです。これにより、これらのモジュールの出力を制御できるだけでなく、テストが外部コードによって汚染されていないことを保証できます。

このサービス内には、モックするモジュールが 2 つあります: PrismaClientrandomColor

次を追加して、これらのモジュールをモックします。

上記では、lib/prisma モジュールは、Vitest の自動モック検出アルゴリズムを使用してモックされました。このアルゴリズムは、"実際の" Prisma モジュールと同じディレクトリで __mocks__ という名前のフォルダーと __mocks__/prisma.ts ファイルを探します。このファイルのエクスポートは、実際のモジュールエクスポートの代わりにモックモジュールとして使用されます。

randomColor モックは、モジュールがデフォルト値のみをエクスポートするため、少し異なります。デフォルト値は関数です。vi.mock の 2 番目のパラメーターは、モジュールがインポートされたときに返すオブジェクトを返す関数です。上記のコードスニペットは、このオブジェクトに default キーを追加し、その値を '#ffffff' の静的な戻り値を持つスパイ可能な関数に設定します。

テストスイートのコンテキスト内では、beforeEachvi.restoreAllMocks を使用して、個々のテストごとにモックが元の状態に復元されるようにします。これは、一部のテストでは、その特定のテストのモックの動作を変更するため重要です。

: これらのモックの仕組みが不明な場合は、モックについて説明したこのシリーズの前の記事を必ず参照してください。

これらのモジュールが TagsService 内にインポートされるたびに、モックバージョンが代わりにインポートされるようになります。

upsertTags 関数のテスト

upsertTags 関数は、タグ名の配列を受け取り、それぞれの名前に対して新しいタグを作成します。ただし、データベースに同じ名前のタグが既に存在する場合、タグは作成されません。この関数の戻り値は、関数に提供されたすべてのタグ名に関連付けられたタグ ID の配列であり、新規と既存の両方が含まれます。

テストスイート内の beforeEach 呼び出しのすぐ下に、upsertTags 関数に関連するテストスイートを記述するための別の describe を追加します。これも、テストの出力をグループ化して、この特定の関数に関連するテストがどれであるかを簡単に確認できるようにするためです。

次に、記述するテストが何をカバーすべきかを決定する時が来ました。upsertTags 関数を見て、それが持つ特定の動作を検討してください。それぞれの望ましい動作はテストされるべきです。

以下に、この関数でテストされるべき各動作を示すコメントが追加されています。コメントには番号が振られており、テストが記述される順序を示しています。

テストするシナリオのリストが準備できたので、それぞれのテストを書き始めることができます。

関数がタグ ID のリストを返すことを検証する

最初のテストでは、関数の戻り値がタグ ID の配列であることを保証します。この関数の describe ブロック内に、新しいテストを追加します。

上記のテストは次のことを行います。

  1. Prisma Client の $transaction 関数のレスポンスをモック化します。
  2. upsertTags 関数を呼び出します。
  3. 関数のレスポンスが、$transaction の期待されるモック化されたレスポンスと等しいことを保証します。

このテストは、関数の望ましい結果を具体的にテストするため重要です。この関数が将来変更された場合でも、このテストは関数の結果が期待どおりであることを保証します。

: Vitest によって提供される特定の方法が何をするか不明な場合は、Vitest のドキュメントを参照してください。

ここで npm run test:unit を実行すると、テストが正常にパスするはずです。

関数がまだ存在しないタグのみを作成することを検証する

上記で計画された次のテストでは、関数がデータベースに重複したタグを作成しないことを検証します。

関数には、タグ名を表す文字列のリストが提供されます。関数は最初にそれらの名前を持つ既存のタグをチェックし、結果に基づいて新しいタグのみを作成するようにフィルタリングします。

テストは次のことを行う必要があります。

  • prisma.tag.findMany の最初の呼び出しをモック化して、単一のタグを返すようにします。これは、関数に提供された名前に基づいて、1 つの既存のタグが見つかったことを意味します。
  • upsertTags を 3 つのタグ名で呼び出します。1 つの名前は、モック化された既存のタグの名前である tag1 である必要があります。
  • prisma.tag.createMany に、tag1 と一致しなかった 2 つのタグのみが提供されたことを保証します。

次のテストを、upsertTags 関数の describe ブロック内の前のテストの下に追加します。

再度 npm run test:unit を実行すると、両方のテストがパスすることが表示されるはずです。

関数が新しいタグにランダムな色を与えることを検証する

この次のテストでは、新しいタグが作成されるたびに、新しいランダムな色が提供されることを検証する必要があります。

これを行うには、3 つの新しいタグを挿入する基本的なテストを作成します。upsertTags 関数が呼び出された後、randomColor 関数が 3 回呼び出されたことを保証できます。

以下のスニペットは、このテストがどのように見えるかを示しています。新しいテストを、upsertTags 関数の describe ブロック内で、前に記述したテストの下に追加します。

npm run test:unit コマンドを実行すると、3 つのテストが成功するはずです。

上記のテストが randomColor が何回呼び出されたかをどのようにチェックできたのか疑問に思うかもしれません。

覚えておいてください、このファイルのコンテキスト内では、randomColor モジュールはモック化されており、そのデフォルトのエクスポートは、静的な文字列値を返す関数を提供する vi.fn であるように構成されています。

vi.fn が使用されたため、モック化された関数は、Vitest 内でスパイできる関数として登録されました。

その結果、現在のテスト中に関数が何回呼び出されたかのカウントなど、特別なプロパティにアクセスできます。

関数が新しく作成されたタグ ID をその戻り値の配列に含めることを検証する

このテストでは、関数が関数に提供されたすべてのタグ名に関連付けられたタグ ID を返すことを検証する必要があります。これは、既存のタグ ID と新しく作成されたタグの ID の両方を返す必要があることを意味します。

このテストは次のことを行う必要があります。

  1. tag.findMany の最初の呼び出しでタグを返すようにして、既存のタグが見つかったことをシミュレートします。
  2. tag.createMany のレスポンスをモック化します。
  3. tag.findMany の 2 回目の呼び出しで 2 つのタグを返すようにして、新しく作成された 2 つのタグが見つかったことを示します。
  4. 3 つのタグで upsertTags 関数を呼び出します。
  5. 3 つの ID すべてが返されることを保証します。

これを実現するために、次のテストを追加します。

npm run test:unit を実行して、上記のテストが機能することを確認します。

関数にタグ名が何も提供されない場合に空の配列を返すことを検証する

予想どおり、この関数にタグ名が何も提供されない場合、タグ ID を返すことはできません。

このテストでは、次のものを追加して、この動作が機能していることを検証します。

これで、この関数に対して決定されたすべてのシナリオがテストされました!

package.json に追加したスクリプトのいずれかを使用してテストを実行すると、すべてのテストが実行され、正常にパスすることが表示されるはずです!

: このコマンドをまだ実行していない場合は、@vitest/ui パッケージをインストールしてコマンドを再実行するように求められる場合があります。

Successful suite of tests

deleteOrphanedTags 関数をテストする

この関数は、前の関数とは非常に異なるシナリオです。

既にお気づきかもしれませんが、この関数は単に Prisma Client 関数の呼び出しをラップするだけです。そのため... ご想像のとおり!この関数は実際にはテストを必要としません!

まとめと今後の展望

この記事の過程で、あなたは

  • ユニットテストとは何か、そしてそれがアプリケーションにとってなぜ重要かを学びました。
  • ユニットテストが厳密には必要ではない状況のいくつかの例を見ました。
  • Vitest のセットアップ
  • テストを記述する際に生活を楽にするためのいくつかのコツを学びました。
  • API 内のサービスのユニットテストの記述を試しました。

この記事では quotes API の 1 つのファイルのみを取り上げましたが、tags サービスをテストするために使用された概念と方法は、アプリケーションの残りの部分にも適用できます。API の残りの部分についてもテストを書いて練習することをお勧めします!

このシリーズの次のパートでは、統合テストに深く入り込み、この同じアプリケーションの統合テストを記述します。

次回の投稿をお見逃しなく!

Prisma ニュースレターに登録する