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サービスを介してメールを送信する

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

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

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

このような状況では、代わりにこれらの依存関係をモックすることが一般的なプラクティスであり、実際のインポートされたオブジェクトを、制御された値を返す「フェイク」に置き換えます。そうすることで、別のモジュールの動作を考慮することなく、テスト対象の関数で特定の状態をトリガーする能力が得られます。

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

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

テストを書き始める前に、試すためのプロジェクトが必要です。プロジェクトをセットアップするには、Prismaを使ったサンプルプロジェクトを素早くセットアップできるツールであるtry-prismaを使用します。

ターミナルで次のコマンドを実行してください

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

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

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

Vitestのセットアップ

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

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

VitestフレームワークとそのCLIツールをインストールするために、プロジェクトでこのコマンドを実行してください

次に、プロジェクトのルートにtestという名前の新しいフォルダを作成し、すべてのテストを配置します

:Vitestでは、テストを/testフォルダに入れる必要はありません。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属性があり、そのカラムがデータベースに一意なインデックスを持つことを示します。これにより、テストの2回目以降の実行でエラーが発生します。
  • このテストは開発データベースに対して実行されていることを前提としており、データベースが利用可能であることを必要とします。このテストを実行するたびに、レコードがデータベースに追加されます。

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

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

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.tsでのprisma.user.createの呼び出しは、もはやデータベースにアクセスしていません。現在、その関数は実質的に何もしないでundefinedを返します。

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

test/sample.test.tsファイルに、その関数が個々のテスト中にどのように動作すべきかをVitestに伝えるために、以下を追加します

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

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

Mock Functions

上記のコードスニペットで使用されているmockResolvedValueは、通常のprisma.user.create関数を、指定された値を返す関数に置き換えます。その単一のテストの間、その関数は次の代入を行ったかのように動作します

:この記事の後半では、モックされたPrisma Clientで利用できる便利な関数のいくつかについて、およびそれらをどのように使用するかについて詳しく掘り下げます。

これで、Prisma Clientを使用する関数を、事前にクライアントの動作をモックして、望ましい結果を保証しながら実行できます。このようにして、個々のクエリについて心配する代わりに、関数の実際のビジネスロジックに集中できます。

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

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

モックされたPrisma Clientインスタンスを手に入れ、関数内の特定のシナリオをテストするために必要なクエリ結果を生成できるようになりました...次はどうしますか?

この記事の残りの部分では、モックされたクライアントとVitestで利用可能な多くの関数と、それらがさまざまなシナリオでテスト体験を可能にするためにどのように使用されるかについて詳しく掘り下げます。

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

クエリ応答のモック

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

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

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

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

その結果、unpublishedpublished配列は、publishedプロパティのtrue値を含め、まったく同じ値を持つことになります。

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

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

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

エラーのトリガーと捕捉

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

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

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

一見すると少し面倒に思えるかもしれませんが、このケースで関数の動作を手動で定義する必要があることは、実際には追加の利点です。これにより、関数が異なる状態(エラー状態も含む)でどのような出力を出すかについて、きめ細かな制御が可能になります。

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

expect関数の応答にrejectsキーワードを使用することで、Vitestはexpectに与えられたPromiseを解決し、エラー応答を探すことを認識します。Promiseが解決されると、toThrowtoThrowError関数を使用して、エラーに関する特定の詳細をチェックできます。

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

モックする必要があるPrisma Clientの別の部分は、$transactionです。

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

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

例としては、次のようになります

上記のテストでは

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

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

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

重要なビジネスロジックを持つインタラクティブトランザクションをテストするには、次のようなテストを記述する場合があります

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

何が起こるかというと

  1. 投稿と応答のオブジェクトがモックされる。
  2. createcountメソッドの応答がモックされる。
  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技術運営委員会のメンバーであり、他にも素晴らしい功績をあげています)は、最近のライブストリームでこの問題を非常によく説明しています。

問題の核心は、JestがデフォルトではエラーがErrorクラスのインスタンスであるかどうかを判断できないことです。

これにより、アプリケーションのさまざまなケースのテストを作成する際に、予期せぬ問題が発生する可能性があります。

それらはどう違うのか?

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

関数の命名規則や設定など、ごくわずかな調整が必要になるだけです。

Jestを使用すべきか?

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

まとめと今後の展望

この記事では、アプリケーションの単体テストにおいて重要な役割を果たすモックスパイの概念に焦点を当てました。具体的には、次のことを探求しました

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

テストの世界におけるこの知識とコンテキストがあれば、アプリケーションを単体テストするために必要なツールセットが手に入りました。このシリーズの次の記事では、まさにそれを行います!

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

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

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

© . All rights reserved.