2022年12月22日

Prisma でのテストの究極ガイド: Prisma Client のモック

アプリケーションが成長するにつれて、自動テストはますます重要になります。この記事では、実際のデータベースにアクセスせずにデータベースインタラクションを含む関数をテストできるように、Prisma Client をモックする方法を学びます。

The Ultimate Guide to Testing with Prisma: Mocking Prisma Client

目次

はじめに

テストは、開発者が書くコードに自信を持ち、製品をより効率的に反復できるようにするため、アプリケーションにおいてますます重要になっています。

自信を持って効率的に作業できることは、想像できるように、すべての開発者のワークフローにおいて重要な側面です。では… なぜすべての開発者がアプリケーションのテストを書かないのでしょうか?この質問に対する答えは多くの場合、「テストを書くこと、特にデータベースが関与する場合、トリッキーになる可能性がある!」ということです。

Testing meme

警告: 悪いアドバイス 👆🏻

このシリーズでは、データベースと対話するさまざまなアプリケーションに対して、さまざまな種類のテストを実行する方法を学びます。

この記事では特に、モックのトピックを掘り下げ、Prisma Client をモックする方法を順を追って説明します。次に、モックされたクライアントで何ができるかを見ていきます。

使用する技術

前提条件

前提知識

このシリーズに入るにあたって、以下の知識があると役立ちます。

  • JavaScript または TypeScript の基本的な知識
  • Prisma Client とその機能の基本的な知識

開発環境

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

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

モックとは?

このシリーズで最初に見る概念はモックです。この用語は、置き換える実際のオブジェクトと同様に動作する、オブジェクトの制御された代替物を作成する手法を指します。

モックの目的は通常、関数が必要とする可能性のある外部依存関係を開発者が置き換え、その関数に対して効果的にユニットテストを作成できるようにすることです。これにより、テストは直接関係のない外部モジュールの動作を気にすることなく、関数の動作に隔離できます。

: このシリーズの次の記事で、ユニットテストを詳しく見ていきます。

これを説明するために、次の関数を考えてみましょう。

この関数は 3 つのことを行います。

  1. 有効なメールアドレスが提供されていることを確認します。
  2. 無効なアドレスが提供された場合はエラーをスローします。
  3. 仮想の mailer サービスを介してメールを送信します。

この関数が期待どおりに動作することを検証するテストを作成するには、まず、関数に無効なメールアドレスが提供されたシナリオをテストし、エラーがスローされることを検証することから始めるでしょう。

ただし、この関数は isValidEmailmailer の 2 つの外部コードに依存しています。これらは別のコードであり、技術的にはテストしている関数とは無関係であるため、これらのインポートが適切に機能するかどうかを心配する必要はありません。代わりに、これらは機能的であり、個別にテストされるべきであると想定する必要があります。

また、mailer.send() が呼び出されたときに、実際のメールがテスト中に送信されることも望ましくありません。これは、その機能がテストしている関数とは独立しているためです。

このような状況では、これらの依存関係をモックし、実際のインポートされたオブジェクトを制御された値を返す「偽物」に置き換えるのが一般的な方法です。そうすることで、別のモジュールの動作を考慮することなく、テストのターゲット関数で特定の状態をトリガーする機能を得ることができます。

これはモックがどのように役立つかを示すかなり基本的なシナリオですが、この記事の残りの部分では、モジュールをモックし、それらのモックを使用して特定のシナリオをテストするために使用できるさまざまなパターンとツールについて詳しく説明します。

Prisma プロジェクトのセットアップ

テストの作成に入る前に、実験するプロジェクトが必要です。セットアップするには、Prisma を使用したサンプルプロジェクトをすばやくセットアップできるツールである try-prisma を使用します。

ターミナルで次のコマンドを実行します。

完了すると、スタータープロジェクトが現在の作業ディレクトリの mocking_playground という名前のフォルダーにセットアップされているはずです。

また、ターミナルに追加の出力が表示され、次のステップに関する指示が表示されます。これらの指示に従ってプロジェクトに入り、最初の Prisma マイグレーションを実行します。

これで SQLite データベースが生成され、スキーマが適用され、Prisma Client が生成されました。プロジェクトでの作業を開始する準備ができました!

Vitest のセットアップ

テストとモックを作成するには、テストフレームワークが必要です。このシリーズでは、ますます人気が高まっている Vitest テストフレームワークを使用します。これは、テストを構築および実行したり、モジュールのモックを作成したりできるツールセットを提供します。

: Vitest は、他にもたくさんの素晴らしいことを行います。興味があれば、ドキュメントをご覧ください。

プロジェクトでこのコマンドを実行して、Vitest フレームワークとその CLI ツールをインストールします。

次に、プロジェクトのルートに test という名前の新しいフォルダーを作成します。これがすべてのテストの保存場所になります。

: テストを /test フォルダーに入れることは、Vitest では必須ではありません。Vitest は、これらの 命名規則に基づいて、デフォルトでテストファイルを検出します。

最後に、package.json で、vitest コマンドを実行するだけの test という名前の新しいスクリプトを追加します。

これで、npm run test を使用してテストを実行できます。また、短縮形として npm t を実行することもできます。現在、テストファイルがないため、テストは失敗します。

/test ディレクトリ内に sample.test.ts という名前の新しいファイルを作成します。

Vitest が正しくセットアップされていることを確認できるように、次のテストを追加します。

有効なテストができたので、npm t を実行すると成功するはずです。Vitest がセットアップされ、使用できる状態になりました。

Prisma Client をモックする理由

ユニットテストで Prisma Client のモックがなぜ役立つのかを説明する最良の方法は、Prisma Client を使用する関数を作成し、モックされたクライアントを使用しないその関数のテストを作成することです。

プロジェクトのルートに、libs という名前の新しいフォルダーを作成します。次に、そのフォルダー内に prisma.ts という名前のファイルを作成します。

次のスニペットをその新しいファイルに追加します。

上記のコードは、Prisma Client をインスタンス化し、シングルトンインスタンスとしてエクスポートします。これは「本物」の Prisma Client インスタンスです。

使用可能な Prisma Client の使用可能なインスタンスができたので、それを使用する関数を作成します。

script.ts の内容を次のように置き換えます。

createUser 関数は次のことを行います。

  1. user 引数を受け取ります。
  2. userprisma.user.create 関数に渡します。
  3. 応答を返します。これは新しいユーザーオブジェクトである必要があります。

次に、その新しい関数のテストを作成します。このテストでは、createUser が有効なユーザー (新しいユーザー) を指定されたときに期待されるデータを返すことを確認します。

test/sample.test.ts を更新して、以下のスニペットと一致するようにします。

: 上記のテストは、モックされた Prisma Client を使用していません。実際のクライアントインスタンスを使用して、実際のデータベースに対してテストするときに発生する可能性のある問題を示しています。

データベースにまだユーザーレコードが含まれていないと仮定すると、このテストは最初に実行したときに合格するはずです。ただし、いくつかの問題があります。

  • 次回このテストを実行すると、作成されたユーザーの id1 にならず、テストが失敗します。
  • email フィールドには、Prisma スキーマに @unique 属性があり、その列がデータベースに一意のインデックスを持っていることを示しています。これにより、テストを後続で実行するとエラーが発生します。
  • このテストでは、開発データベースに対して実行していることを前提としており、データベースが利用可能である必要があります。このテストを実行するたびに、レコードがデータベースに追加されます。

単一の関数に焦点を当てたユニットテストなどの状況では、データベース操作は正しく動作すると仮定し、代わりにクライアントまたはドライバーのモックバージョンを使用するのがベストプラクティスです。これにより、ターゲットとする関数の特定の動作のテストに集中できます。

: データベースに対してテストし、実際に操作を実行したい場合があるシナリオがあります。結合テストとエンドツーエンドテストは、これらのケースの良い例です。これらのテストは、アプリケーションの複数の関数と領域にまたがって発生する複数のデータベース操作に依存する場合があります。

Prisma Client のモック

前のセクションで概説した理由から、Prisma Client を使用する関数を適切にユニットテストするには、クライアントのモックを作成するのがベストプラクティスと見なされています。このモックは、関数が通常使用するインポートされたモジュールを置き換えます。

これを実現するには、Vitest のモックツールと、vitest-mock-extended という名前の外部ライブラリを使用します。

まず、vitest-mock-extended をプロジェクトにインストールします。

次に、test/sample.test.ts ファイルに移動し、Vitest に libs/prisma.ts モジュールをモックする必要があることを知らせるために、次の変更を加えます。

vi オブジェクトで使用可能な mock 関数は、Vitest に、指定されたファイルパスにあるモジュールをモックする必要があることを知らせます。ドキュメントで説明されているように、mock 関数がターゲットモジュールをモックする方法を決定できる方法はいくつかあります。

現在、Vitest は '../libs/prisma' にあるモジュールをモックしようとしますが、prisma オブジェクトの「深い」または「ネストされた」プロパティを自動的にモックすることはできません。たとえば、prisma.user.create() は、Prisma Client インスタンスの深くネストされたプロパティであるため、適切にモックされません。これにより、関数が通常どおりに実際のデータベースに対して実行されるため、テストが失敗します。

この問題を解決するには、そのモジュールを正確にどのようにモックするかを Vitest に知らせ、モックされたモジュールがインポートされたときに返される値を指定する必要があります。これには、深くネストされたプロパティのモックバージョンを含める必要があります。

libs ディレクトリ内に __mocks__ という名前の新しいフォルダーを作成します。

フォルダー名 __mocks__ は、モジュールの手動で作成されたモックを配置できるテストフレームワークでの一般的な規則です。__mocks__ フォルダーは、モックしているモジュールのすぐ隣にある必要があります。そのため、libs/prisma.ts ファイルの隣にフォルダーを作成しました。

その新しいフォルダー内に、prisma.ts という名前のファイルを作成します。

このファイルの名前が「本物」のファイルである prisma.ts と同じであることに注意してください。この規則に従うことで、Vitest は vi.mock を介してモジュールをモックするときに、そのファイルを使用してクライアントのモックバージョンを見つける必要があることを認識します。

その構造が整ったので、手動モックを作成します。

新しい libs/__mocks__/prisma.ts ファイルに、次を追加します。

上記のスニペットは次のことを行います。

  1. モックされたクライアントを作成するために必要なすべてのツールをインポートします。
  2. 個々のテストごとに、モックを元の状態にリセットする必要があることを Vitest に知らせます。
  3. vitest-mock-extended ライブラリの mockDeep 関数を使用して、Prisma Client の「ディープモック」を作成およびエクスポートします。これにより、オブジェクトのすべてのプロパティ (深くネストされたプロパティも含む) がモックされることが保証されます。

: 基本的に、mockDeep は、すべての Prisma Client 関数の値を Vitest ヘルパー関数である vi.fn() に設定します。

この時点で、npm t でテストを再度実行すると、以前と同じエラーが表示されなくなっているはずです。しかし、まだ問題があります…

Failed test

クエリが `undefined` を返す

このエラーは実際には、モックが正しく配置されたために発生します。script.tsprisma.user.create 呼び出しは、データベースにアクセスしなくなりました。現在、その関数は本質的に何もせず、undefined を返します。

Vitest に prisma.user.create が何をする必要があるかをモックすることで伝える必要があります。適切なモックバージョンの Prisma Client ができたので、テストに簡単な変更を加える必要があります。

test/sample.test.ts ファイルで、次のコードを追加して、その個々のテストの過程で関数がどのように動作するかを Vitest に伝えます。

上記では、「偽」のクライアントは、Prisma Client のディープモックをエクスポートするためインポートされました。

このオブジェクトでは、各 Prisma Client プロパティと関数に新しい関数セットがアタッチされていることに気付くでしょう。

Mock Functions

上記のスニペットで使用されている mockResolvedValue は、通常の prisma.user.create 関数を、指定された値を返す関数に置き換えます。その単一のテストの過程では、その関数は次の代入を実行した場合と同じように動作します。

: この記事の後半では、モックされた Prisma Client で使用できる役立つ関数とその使用方法について詳しく説明します。

これで、Prisma Client の動作を事前にモックして目的の結果を保証することで、Prisma Client を使用する関数を実行できるようになりました。これにより、個々のクエリを心配するのではなく、関数の実際のビジネスロジックに集中できます。

ここでテストを再度実行すると、すべてのテストが合格したことが最終的に表示されるはずです! ✅

モックされたクライアントの使用

モックされた Prisma Client インスタンスを取得し、クライアントを操作して、関数で特定のシナリオをテストするために必要なクエリ結果を生成する機能を手に入れたとしましょう… 次は何でしょうか?

この記事の残りの部分では、モックされたクライアントと Vitest が使用できる多くの関数と、それらがさまざまなシナリオでどのように使用されてテストエクスペリエンスを有効にするかについて詳しく説明します。

: 以下の例は、実行可能な本格的なユニットテストではありません。むしろ、モックされたクライアントを介して使用可能なツールの機能サンプルになります。このシリーズの次の記事では、ユニットテストについて詳しく説明します。

クエリ応答のモック

モックされたクライアントを使用する最も一般的なことの 1 つは、クエリの応答をモックすることです。この記事の前半で、create メソッドの応答をすでにモックしましたが、これを行う方法は複数あり、それぞれに独自のユースケースがあります。

たとえば、このシナリオを見てみましょう。

: ここでの toStrictEqual の使用法は重要です。オブジェクトを比較する場合、toStrictEqual はオブジェクトが同じ構造と型を持っていることを保証します。

このテストは正常に合格しますが、あまり意味がありません。prisma.post.findMany.mockResolvedValue が呼び出されると、その関数に提供された値は、テストの残りの部分の prisma.post.findMany の応答として使用されます。より具体的には、libs/__mocks__/prisma.tsmockReset 関数が呼び出されるまでです。

その結果、unpublished 配列と published 配列には、published プロパティの true 値を含め、まったく同じ値が含まれます。

このシナリオでより現実的な応答を生成するために、別の関数 (mockResolvedValueOnce) を使用できます。この関数は、関数の応答と後続の呼び出しの応答をモックするために複数回呼び出すことができます。

getPosts 関数では、mockResolvedValueOnce を使用して、その関数が返す最初の応答と 2 番目の応答をモックできます。

: Vitest を介して使用可能な多くの関数には、mockXValue とともに mockXValueOnce メソッドがあります。詳細については、ドキュメントを参照してください。

エラーのトリガーとキャプチャ

テストしたい別のシナリオは、クエリが失敗してエラーを返すかスローする場合です。これが役立つ可能性のある優れた例は、Prisma Client の findUniqueOrThrow 関数です。

この関数は一意のレコードを検索しますが、レコードが見つからない場合はエラーをスローします。ただし、Prisma Client の関数はモックされているため、findUniqueOrThrow 関数はそのように動作しなくなりました。エラー状態を手動でトリガーする必要があります。この動作をテストする方法の例を以下に示します。

mockImplementation を使用すると、モックされた関数の動作を置き換える関数を提供できます。上記の例では、置換関数は単にエラーをスローします。

最初は少し面倒に見えるかもしれませんが、この場合に関数の動作を手動で定義する必要があることは、実際には追加の利点です。これにより、エラー状態であっても、さまざまな状態での関数の出力を細かく制御できます。

上記と同様に、テストしているメソッドがエラーに関連するメッセージを返すのではなく、実際のエラーをスローすることを意図している場合も、それをテストできます!

expect 関数の応答で rejects キーワードを使用すると、Vitest は expect に指定された Promise を解決し、エラー応答を探すことを認識します。Promise が解決されると、toThrow 関数と toThrowError 関数を使用して、エラーに関する特定の詳細を確認できます。

トランザクションのモック

モックする必要がある可能性のある Prisma Client のもう 1 つの要素は、$transaction です。

トランザクションには、シーケンシャル操作インタラクティブトランザクションの 2 種類があります。これらのモックの方法は、テストの目標と $transaction を使用しているコンテキストによって大きく異なります。ただし、この関数をモックする一般的な方法は 2 つあります。

シーケンシャル操作とインタラクティブトランザクションの両方で、完了したトランザクションの結果は最終的に $transaction 関数から返されます。テストがトランザクションの結果のみを気にする場合、テストは関数の応答をモックした上記のテストと非常によく似ています。

例は次のようになります。

上記のテストでは、

  1. 作成する予定の投稿のデータをモックしました。
  2. $transaction からの応答がどのように見えるかをモックしました。
  3. Prisma Client メソッドがモックされた後に関数を呼び出しました。
  4. 関数から返された値が、期待どおりの値と一致することを確認しました。

$transaction 関数自体の応答をモックすることにより、トランザクションのシーケンシャルアクション (またはインタラクティブトランザクションが該当する場合) 内で何が起こったかを心配する必要はありませんでした。

検証する必要がある重要なビジネスロジックを持つインタラクティブトランザクションをテストする場合はどうでしょうか?この方法では、トランザクションの内部動作を完全に無視するため、機能しません。

重要なビジネスロジックを持つインタラクティブトランザクションをテストするには、次のようなテストを作成できます。

このテストは、考慮すべきさまざまな可動部品が多いため、少し複雑です。

これが起こることです。

  1. 投稿オブジェクトと応答オブジェクトがモックされます。
  2. create メソッドと count メソッドの応答がモックされます。
  3. $transaction 関数の実装がモックされ、実際のクライアントインスタンスではなく、モックされた Prisma Client をインタラクティブトランザクション関数に提供できるようにします。
  4. addPost メソッドが呼び出されます。
  5. 応答の値が検証され、インタラクティブトランザクション内のビジネスロジックが機能することを確認します。より具体的には、新しい投稿の published フラグが true に設定されていることを確認します。

メソッドのスパイ

最後に探求する概念は、スパイです。Vitest は、TinySpy という名前のパッケージを介して、関数をスパイする機能を提供します。スパイを使用すると、コードの実行中に関数を監視し、呼び出された回数、渡されたパラメーター、返された値などを判断できます。

: 関数をスパイすると、ターゲット関数またはその動作を変更せずに、コードの実行中に関数の詳細を観察できます。

vi.spyOn() を使用してモックされていない関数をスパイできますが、vi.fn() を使用したモックされた関数は、デフォルトですべてのスパイ機能を使用できます。Prisma Client はモックされているため、すべての関数をスパイできるはずです。

Spy functions.

以下は、スパイを使用したテストの例です。

これらのスパイ関数は、さまざまな入力に基づいて特定のシナリオがトリガーされることを確認しようとする場合に特に役立ちます。

なぜ Vitest なのか?

この記事が、Jest のような、より確立された人気のあるフレームワークではなく、Vitest をテストフレームワークとして重点を置いている理由について疑問に思うかもしれません。

この決定の背景にある理由は、異なるツールと Node.js との互換性、特に Error オブジェクトの取り扱いに関連しています。 Matteo Collina 氏は、Node.js Technical Steering Committee のメンバーであり、その他にも素晴らしい実績を持つ人物ですが、最近の彼のライブストリームでこの問題について非常に分かりやすく説明しています。

問題を簡単に言うと、Jest はエラーが Error クラスのインスタンスであるかどうかを標準で判別できません。

これは、アプリケーションのさまざまなケースに対するテストを作成する際に、予期しないさまざまな問題を引き起こす可能性があります。

どのように違うのですか?

幸いなことに、ほとんどのテストフレームワークは非常によく似ており、概念は比較的スムーズに移行できます。たとえば、Jest の使用に慣れていて、Vitest や node-tap (別のテストフレームワーク) のようなものへの移行を検討している場合、すでに持っている知識は新しいテクノロジーに非常に転用可能です。

非常に小さな調整が必要になります。たとえば、関数の命名規則や設定などです。

Jest を使うべき時はありますか?

はい! Jest は非常に有能な人々によって書かれた素晴らしいツールです。Vitest が Node.js でバックエンドアプリケーションをテストする際の「最適なツール」であるかもしれませんが、Jest はフロントエンドの JavaScript アプリケーションをテストするのに十分な能力を持っています。

まとめ & 次のステップ

この記事では、アプリケーションのユニットテストで重要な役割を果たす モッキングスパイ の概念に焦点を当てました。具体的には、以下を探求しました。

  • モッキングとは何か、そしてなぜそれが役立つのか
  • Vitest と Prisma を設定したプロジェクトをセットアップする方法
  • Prisma Client をモックする方法
  • モックされた Prisma Client インスタンスを使用する方法

テストの世界に関するこの知識と背景があれば、アプリケーションのユニットテストに必要なツールセットが揃いました。このシリーズの次の記事では、まさにそれを行います!

Prisma Client を使用するアプリケーションをテストするさまざまな方法を探求するこのシリーズの次のパートにもぜひご参加ください。

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

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