2023年1月31日

Prismaを使ったテストの究極ガイド:単体テスト

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

The Ultimate Guide to Testing with Prisma: Unit Testing

目次

はじめに

単体テストは、アプリケーション内の個々のコード単位(例:関数)が期待どおりに機能することを確認するための主要な方法の一つです。

テストに慣れていない人にとって、単体テストとは何かを理解するのは非常に難しい場合があります。自分のアプリケーションがどのように機能するか、テストの書き方、テスト環境の準備方法を理解するだけでなく、何をテストすべきかについても理解しなければなりません!

そのため、開発者はしばしば次のようなテストアプローチをとります

: 彼の正直さに感謝します 😉 (@RoxCodes)

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

このシリーズを通して、コードの様々な領域を検討し、何をテストすべきか、なぜテストが必要なのか、そしてそれらのテストをどのように書くかについて解説します。これには、単体テスト結合テストエンドツーエンドテスト、そしてそれらのテストを実行する継続的インテグレーション(CI)と継続的デプロイメント(CD)のワークフローの設定が含まれます。

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

単体テストとは?

単体テストは、小さく分離されたコード片に対してテストを作成するテストの一種です。単体テストは、コードの小さな単位を対象とし、さまざまな状況で期待どおりに機能することを確認します。

通常、単体テストは個々の関数を対象とします。なぜなら、JavaScriptアプリケーションにおいて関数は通常、最小の単一コード単位であるためです。

以下の関数を例にとってみましょう

この関数は単純ですが、単体テストの良い候補です。単一の機能セットが1つの関数にまとめられています。この関数が正しく機能することを確認するために、文字列'abcde'を渡し、文字列'edcba'が返されることを確認できます。

関連するテストスイート、つまりテストセットは以下のようになるでしょう

上記で気づいたかもしれませんが、単体テストの目的は、アプリケーションの最小の構成要素が正しく機能することを確認することだけです。これを行うことで、それらの構成要素を組み合わせ始めたときに、結果として得られる動作が予測可能であるという自信が生まれます。

Test graphic

これが非常に重要である理由は上記で説明されています。単体テストを実行する際、すべてのテストがパスすれば、すべての構成要素が機能しており、その結果としてアプリケーションが意図どおりに動作していることを確信できます。しかし、1つでもテストが失敗した場合、アプリケーションが意図どおりに動作していないと仮定でき、失敗したテストに基づいて何が問題なのかを正確に知ることができます。

単体テストではないもの

単体テストでは、カスタムコードが意図どおりに機能することを確認することが目的です。前の文で注目すべき重要な点は、「カスタムコード」というフレーズです。

JavaScript開発者として、あなたはnpmを介してコミュニティによって構築された豊富なモジュールとパッケージのエコシステムにアクセスできます。外部ライブラリを使用することで、独自に車輪を再発明する手間を省き、多くの時間を節約できます。

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

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

次の関数を例にとってみましょう

この関数は、正方形の一辺の長さを入力として受け取り、その正方形の一意の色を含む、より定義された正方形を表すオブジェクトを返します。

上記の関数の単体テストを作成する際には、次の点を検証するとよいでしょう

  • 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ファイルを作成します

このファイルには、値として任意のstringを設定できるAPI_SECRETという変数と、現時点では空のままで構わない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.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: 上記で作成した設定ファイルを使用してvitest CLIコマンドをUIモードで実行します。これにより、ブラウザで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フレームワークが提供する関数の呼び出しのみです。いくつかのカスタム関数(validateQuoteController.*)も使用されていますが、それらは別のファイルで定義されており、それぞれのコンテキストでテストされます。

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

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

もう一つ指摘すべき重要なシナリオは、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内で、画面上部のオプションバーにあるCode > Preferences > Settingsに移動します。

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

File nesting option in VSCode

次に、これらの設定をさらに下にスクロールすると、Explorer > File Nesting: Patternsセクションが表示されます。

*.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ファイル内で使用されている外部モジュールをモックすることです。これにより、それらのモジュールの出力を制御できるだけでなく、テストが外部コードによって汚染されないようにすることができます。

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

以下を追加してこれらのモジュールをモックします

上記では、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つ見つかったことを示します。
  • 3つのタグ名でupsertTagsを呼び出します。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つのファイルのみを扱いましたが、タグサービスのテストに使用された概念と方法は、アプリケーションの残りの部分にも適用されます。練習のために、APIの残りの部分についてもテストを作成することをお勧めします!

このシリーズの次のパートでは、結合テストについて深く掘り下げ、同じアプリケーションの結合テストを作成します。

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

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

© . All rights reserved.