単体テストでは、個々の独立したコードの単位をテストし、それらが期待どおりに機能することを確認します。この記事では、単体テストを行うべきコードベースの領域を特定する方法、それらのテストを作成する方法、そしてPrisma Clientを使用する関数に対するテストを処理する方法について学びます。
目次
はじめに
単体テストは、アプリケーション内の個々のコード単位(例:関数)が期待どおりに機能することを確認するための主要な方法の一つです。
テストに慣れていない人にとって、単体テストとは何かを理解するのは非常に難しい場合があります。自分のアプリケーションがどのように機能するか、テストの書き方、テスト環境の準備方法を理解するだけでなく、何をテストすべきかについても理解しなければなりません!
そのため、開発者はしばしば次のようなテストアプローチをとります
注: 彼の正直さに感謝します 😉 (@RoxCodes)
このシリーズでは、完全に機能するアプリケーションを使用します。コードベースに唯一不足しているのは、その動作が意図どおりであることを検証するためのテストスイートです。
このシリーズを通して、コードの様々な領域を検討し、何をテストすべきか、なぜテストが必要なのか、そしてそれらのテストをどのように書くかについて解説します。これには、単体テスト、結合テスト、エンドツーエンドテスト、そしてそれらのテストを実行する継続的インテグレーション(CI)と継続的デプロイメント(CD)のワークフローの設定が含まれます。
この記事では特に、コードの特定の領域に焦点を当て、それらに対して単体テストを作成し、それらの領域の個々の構成要素が正しく機能することを確認します。
単体テストとは?
単体テストは、小さく分離されたコード片に対してテストを作成するテストの一種です。単体テストは、コードの小さな単位を対象とし、さまざまな状況で期待どおりに機能することを確認します。
通常、単体テストは個々の関数
を対象とします。なぜなら、JavaScriptアプリケーションにおいて関数は通常、最小の単一コード単位であるためです。
以下の関数を例にとってみましょう
この関数は単純ですが、単体テストの良い候補です。単一の機能セットが1つの関数にまとめられています。この関数が正しく機能することを確認するために、文字列'abcde'
を渡し、文字列'edcba'
が返されることを確認できます。
関連するテストスイート、つまりテストセットは以下のようになるでしょう
上記で気づいたかもしれませんが、単体テストの目的は、アプリケーションの最小の構成要素が正しく機能することを確認することだけです。これを行うことで、それらの構成要素を組み合わせ始めたときに、結果として得られる動作が予測可能であるという自信が生まれます。
これが非常に重要である理由は上記で説明されています。単体テストを実行する際、すべてのテストがパスすれば、すべての構成要素が機能しており、その結果としてアプリケーションが意図どおりに動作していることを確信できます。しかし、1つでもテストが失敗した場合、アプリケーションが意図どおりに動作していないと仮定でき、失敗したテストに基づいて何が問題なのかを正確に知ることができます。
単体テストではないもの
単体テストでは、カスタムコードが意図どおりに機能することを確認することが目的です。前の文で注目すべき重要な点は、「カスタムコード」というフレーズです。
JavaScript開発者として、あなたはnpmを介してコミュニティによって構築された豊富なモジュールとパッケージのエコシステムにアクセスできます。外部ライブラリを使用することで、独自に車輪を再発明する手間を省き、多くの時間を節約できます。
外部モジュールを使用すること自体に問題はありませんが、それらのモジュールを使用する関数をテストする際には考慮すべき点がいくつかあります。最も重要なのは、この点を念頭に置いておくことです
外部パッケージを信頼せず、それに対してテストを書くべきだと感じるのであれば、おそらくその特定のパッケージを使用すべきではありません。
次の関数を例にとってみましょう
この関数は、正方形の一辺の長さを入力として受け取り、その正方形の一意の色を含む、より定義された正方形を表すオブジェクトを返します。
上記の関数の単体テストを作成する際には、次の点を検証するとよいでしょう
- 1未満の数値が提供された場合、関数は
null
を返す - 関数が面積を正しく計算する
- 関数が正しい形状と値を持つオブジェクトを返す
randomColor
関数が1回呼び出された
各正方形が実際に一意の色を持つことを確認するためのテストについては言及がないことに注意してください。これは、randomColor
が外部モジュールであるため、正しく機能すると仮定されているからです。
注:
randomColor
がnpmパッケージ経由で提供されたものか、あるいは別のファイルでカスタムビルドされた関数であったとしても、このコンテキストでは正しく機能すると仮定すべきです。randomColor
があなたが別のファイルで書いた関数であった場合、それはそれ自身の独立したコンテキストでテストされるべきです。「構成要素」を考えてください!
この概念はPrisma Clientにも当てはまるため重要です。アプリケーションでPrismaを使用する場合、Prisma Clientは外部モジュールです。そのため、すべてのテストはクライアントが提供する関数が期待どおりに機能すると仮定すべきです。
使用する技術
前提条件
前提知識
このシリーズを始めるにあたり、以下の知識があると役立ちます
- JavaScriptまたはTypeScriptの基本的な知識
- Prisma Clientとその機能に関する基本的な知識
- Expressの経験があると尚良い
開発環境
提供される例に沿って進めるには、以下が必要です
このシリーズでは、このGitHubリポジトリを頻繁に使用します。リポジトリをクローンし、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を使用します。
以下のコマンドでvitest
とvitest-mock-extended
をインストールすることから始めます
注: 上記の2つのパッケージに関する情報については、このシリーズの最初の記事を必ず読んでください。
次に、Vitestが単体テストの場所と、それらのテストにインポートする必要があるモジュールを解決する方法を認識できるように設定する必要があります。
プロジェクトのルートにvitest.config.unit.ts
という新しいファイルを作成します
このファイルは、Vitestが提供するdefineConfig
関数を使用して、単体テストの設定を定義しエクスポートします
上記では、Vitestに2つのオプションを設定しました
test.include
オプションは、src
ディレクトリ内の*.test.ts
という命名規則に一致するすべてのファイル内でテストを探すようにVitestに指示します。resolve.alias
設定は、ファイルパスのエイリアスを設定します。これにより、ファイルインポートパスを短縮できます。例:src/auth/auth.service
がauth/auth.service
になります。
最後に、テストをより簡単に実行するために、package.json
にVitest CLIコマンドを実行するスクリプトを設定します。
以下の内容をpackage.json
のscripts
セクションに追加します
上記で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フレームワークが提供する関数の呼び出しのみです。いくつかのカスタム関数(validate
やQuoteController.*
)も使用されていますが、それらは別のファイルで定義されており、それぞれのコンテキストでテストされます。
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
ブランチにすべての関数に対する完全なテストセットがあります。
タグサービスのテスト
タグサービスは、upsertTags
とdeleteOrphanedTags
の2つの関数をエクスポートしています。
まず、tags.service.ts
と同じディレクトリにtags.service.test.ts
という新しいファイルを作成します
注: テストを整理する方法はたくさんあります。このシリーズでは、テストの対象となるファイルのすぐ隣にテストを作成します。これはテストのコロケーションとも呼ばれます。
VSCodeを使用しており、v1.64以降の場合、テストとそのターゲットをコロケーションする際にプロジェクトのファイルツリーを整理する便利な機能を利用できます。
VSCode内で、画面上部のオプションバーにあるCode > Preferences > Settingsに移動します。
設定ページ内で、file nesting
と入力してファイルネスト設定を検索します。以下の設定を有効にします

次に、これらの設定をさらに下にスクロールすると、Explorer > File Nesting: Patternsセクションが表示されます。
*.tsという名前の項目が存在しない場合は、作成してください。次に、*.ts項目の値を${capture}.*.ts
に更新します

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

上記では、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
ファイル内で使用されている外部モジュールをモックすることです。これにより、それらのモジュールの出力を制御できるだけでなく、テストが外部コードによって汚染されないようにすることができます。
このサービス内には、PrismaClient
とrandomColor
の2つのモック対象モジュールがあります。
以下を追加してこれらのモジュールをモックします
上記では、lib/prisma
モジュールはVitestの自動モック検出アルゴリズムを使用してモックされました。このアルゴリズムは、「実際の」Prismaモジュールと同じディレクトリにある__mocks__
というフォルダと__mocks__/prisma.ts
ファイルを探します。このファイルのエクスポートが、実際のモジュールのエクスポートの代わりにモックモジュールとして使用されます。
randomColor
のモックは少し異なります。このモジュールはデフォルト値(関数)のみをエクスポートするためです。vi.mock
の2番目のパラメータは、モジュールがインポートされたときに返すべきオブジェクトを返す関数です。上記のコードスニペットは、このオブジェクトにdefault
キーを追加し、その値を静的な戻り値'#ffffff'
を持つスパイ可能な関数に設定しています。
テストスイートのコンテキストでは、beforeEach
とvi.restoreAllMocks
を使用して、個々のテストごとにモックが元の状態に復元されるようにします。これは、特定のテストのためにモックの動作を変更する場合があるため重要です。
注: これらのモックがどのように機能するかわからない場合は、モックについて説明されているこのシリーズの前の記事を参照してください。
これらのモジュールがTagsService
内でインポートされる場合、代わりにモックされたバージョンがインポートされます。
upsertTags
関数のテスト
upsertTags
関数はタグ名の配列を受け取り、各名前に対して新しいタグを作成します。ただし、データベースに同じ名前の既存のタグがある場合は、タグは作成されません。関数の戻り値は、関数に提供されたすべてのタグ名(新規および既存の両方)に関連付けられたタグIDの配列です。
テストスイート内のbeforeEach
呼び出しのすぐ下に、upsertTags
関数に関連するテストスイートを記述するために、別のdescribe
を追加します。これも、テストの出力をグループ化し、この特定の関数に関連するどのテストがパスしたかを簡単に確認できるようにするために行われます。
次に、作成するテストが何をカバーすべきかを決定します。upsertTags
関数を見て、それがどのような特定の動作をするかを考慮します。それぞれの意図する動作はテストされるべきです。
以下に、この関数でテストすべき各動作を示すコメントが追加されています。コメントには番号が付けられており、テストが書かれる順序を示しています
テストするシナリオのリストが準備できたので、それぞれのテストを作成し始めることができます。
関数がタグIDのリストを返すことを検証
最初のテストは、関数の戻り値がタグIDの配列であることを確認します。この関数のdescribe
ブロック内に、新しいテストを追加します
上記のテストは以下を実行します
- Prisma Clientの
$transaction
関数の応答をモックする upsertTags
関数を呼び出す- 関数の応答が、
$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の両方を返す必要があることを意味します。
このテストは以下を行うべきです
tag.findMany
の最初の呼び出しにタグを返させ、既存のタグを見つけることをシミュレートするtag.createMany
の応答をモックするtag.findMany
の2番目の呼び出しに2つのタグを返させ、新しく作成された2つのタグが見つかったことを示す- 3つのタグで
upsertTags
関数を呼び出す - すべての3つのIDが返されることを確認する
これを達成するために以下のテストを追加します
npm run test:unit
を実行して、上記のテストが機能することを確認します。
タグ名が提供されない場合、関数が空の配列を返すことを検証
ご想像のとおり、この関数にタグ名が提供されない場合、タグIDを返すことはできません。
このテストでは、以下のコードを追加してこの動作が機能していることを検証します
これで、この関数について決定されたすべてのシナリオがテストされました!
package.json
に追加したスクリプトのいずれかを使用してテストを実行すると、すべてのテストが正常に実行され、パスしていることを確認できるはずです!
注: このコマンドをまだ実行していない場合、
@vitest/ui
パッケージのインストールを促され、コマンドを再実行する必要があるかもしれません。

deleteOrphanedTags
関数のテスト
この関数は、前の関数とは非常に異なるシナリオです。
すでにお気づきかもしれませんが、この関数はPrisma Client関数の呼び出しを単にラップしているだけです。そのため...お察しの通り!この関数は実際にはテストを必要としません!
まとめと次回の内容
この記事を通して、あなたは
- 単体テストとは何か、そしてそれがアプリケーションにとってなぜ重要なのかを学びました
- 単体テストが厳密には必要ない状況のいくつかの例を見ました
- Vitestのセットアップ
- テスト作成を簡単にするためのいくつかのコツを学びました
- APIのサービスに対する単体テストの作成を試みました
この記事ではquotes APIの1つのファイルのみを扱いましたが、タグサービスのテストに使用された概念と方法は、アプリケーションの残りの部分にも適用されます。練習のために、APIの残りの部分についてもテストを作成することをお勧めします!
このシリーズの次のパートでは、結合テストについて深く掘り下げ、同じアプリケーションの結合テストを作成します。
次回の投稿をお見逃しなく!
Prismaニュースレターに登録する