統合テストを実際に行うためのNode.jsガイド
公開: 2022-03-11統合テストは恐ろしいものではありません。 これらは、アプリケーションを完全にテストするための重要な部分です。
テストについて話すとき、私たちは通常、コードの小さなチャンクを分離してテストする単体テストについて考えます。 ただし、アプリケーションはその小さなコードチャンクよりも大きく、アプリケーションのほとんどの部分が単独で機能しません。 これは、統合テストがその重要性を証明する場所です。 統合テストは、単体テストが不足している箇所をピックアップし、単体テストとエンドツーエンドテストの間のギャップを埋めます。
この記事では、APIベースのアプリケーションの例を使用して、読み取り可能で構成可能な統合テストを作成する方法を学習します。
この記事のすべてのコード例にはJavaScript/Node.jsを使用しますが、説明するほとんどのアイデアは、任意のプラットフォームでの統合テストに簡単に適合させることができます。
単体テストと統合テスト:両方が必要
単体テストは、特定のコード単位に焦点を当てています。 多くの場合、これは特定の方法またはより大きなコンポーネントの機能です。
これらのテストは個別に実行され、通常、すべての外部依存関係はスタブまたはモックされます。
つまり、依存関係は事前にプログラムされた動作に置き換えられ、テストの結果がテスト対象のユニットの正確さによってのみ決定されるようにします。
ユニットテストの詳細については、こちらをご覧ください。
単体テストは、優れた設計で高品質のコードを維持するために使用されます。 また、コーナーケースを簡単にカバーすることもできます。
ただし、欠点は、単体テストではコンポーネント間の相互作用をカバーできないことです。 ここで統合テストが役立ちます。
統合テスト
単体テストがコードの最小単位を個別にテストすることによって定義される場合、統合テストは正反対です。
統合テストは、相互作用する複数のより大きなユニット(コンポーネント)をテストするために使用され、場合によっては複数のシステムにまたがることもあります。
統合テストの目的は、次のようなさまざまなコンポーネント間の接続と依存関係のバグを見つけることです。
- 無効または誤った順序の引数を渡す
- 壊れたデータベーススキーマ
- 無効なキャッシュ統合
- ビジネスロジックの欠陥またはデータフローのエラー(テストはより広い視野から行われるようになったため)。
テストしているコンポーネントに複雑なロジックがない場合(たとえば、循環的複雑度が最小のコンポーネント)、統合テストは単体テストよりもはるかに重要になります。
この場合、単体テストは主に優れたコード設計を実施するために使用されます。
単体テストは関数が適切に記述されていることを確認するのに役立ちますが、統合テストはシステムが全体として適切に機能していることを確認するのに役立ちます。 したがって、単体テストと統合テストはそれぞれ独自の補完的な目的を果たし、包括的なテストアプローチには両方が不可欠です。
ユニットテストと統合テストは、同じコインの両面のようなものです。 コインは両方なしでは無効です。
したがって、統合テストと単体テストの両方を完了するまで、テストは完了しません。
統合テスト用のスイートをセットアップする
単体テスト用のテストスイートの設定は非常に簡単ですが、統合テスト用のテストスイートの設定は多くの場合より困難です。
たとえば、統合テストのコンポーネントには、データベース、ファイルシステム、電子メールプロバイダー、外部支払いサービスなど、プロジェクト外の依存関係がある場合があります。
場合によっては、統合テストでこれらの外部サービスとコンポーネントを使用する必要があり、スタブ化されることもあります。
それらが必要な場合、それはいくつかの課題につながる可能性があります。
- 脆弱なテストの実行:外部サービスが利用できない、無効な応答を返す、または無効な状態になっている可能性があります。 場合によっては、これが誤検知になることもあれば、誤検知になることもあります。
- 実行が遅い:外部サービスの準備と接続が遅くなる可能性があります。 通常、テストはCIの一部として外部サーバーで実行されます。
- 複雑なテストセットアップ:外部サービスは、テストに必要な状態である必要があります。 たとえば、データベースには必要なテストデータなどをプリロードする必要があります。
統合テストを作成する際に従うべき指示
統合テストには、単体テストのような厳密なルールはありません。 それにもかかわらず、統合テストを作成するときに従うべきいくつかの一般的な指示があります。
繰り返し可能なテスト
テストの順序や依存関係によってテスト結果が変わることはありません。 同じテストを複数回実行すると、常に同じ結果が返されます。 テストでインターネットを使用してサードパーティのサービスに接続している場合、これを実現するのは難しい場合があります。 ただし、この問題はスタブとモックで回避できます。
より詳細に制御できる外部依存関係の場合、統合テストの前後にステップを設定すると、テストが常に同じ状態から開始して実行されるようになります。
関連するアクションのテスト
考えられるすべてのケースをテストするには、単体テストの方がはるかに優れたオプションです。
統合テストはモジュール間の接続に重点を置いているため、モジュール間の重要な接続をカバーするため、通常、満足のいくシナリオをテストする方法があります。
わかりやすいテストとアサーション
テストのクイックビューは、何がテストされているか、環境がどのようにセットアップされているか、何がスタブされているか、いつテストが実行されているか、何がアサートされているかを読者に通知する必要があります。 アサーションは単純で、比較とロギングを改善するためにヘルパーを利用する必要があります。
簡単なテストセットアップ
テストを初期状態にすることは、可能な限り単純で理解しやすいものでなければなりません。
サードパーティのコードのテストは避けてください
テストではサードパーティのサービスを使用できますが、テストする必要はありません。 そして、あなたがそれらを信頼しないのなら、あなたはおそらくそれらを使うべきではありません。
プロダクションコードをテストコードなしのままにする
プロダクションコードはクリーンでわかりやすいものにする必要があります。 テストコードと本番コードを混在させると、接続できない2つのドメインが結合されます。
関連するロギング
失敗したテストは、適切なログがなければあまり価値がありません。
テストに合格すると、追加のログは必要ありません。 しかし、それらが失敗した場合、大規模なロギングが不可欠です。
ログには、すべてのデータベースクエリ、APIリクエストとレスポンス、およびアサートされているものの完全な比較が含まれている必要があります。 これにより、デバッグが大幅に容易になります。
良いテストはきれいでわかりやすいように見えます
ここでのガイドラインに従う簡単なテストは、次のようになります。
const co = require('co'); const test = require('blue-tape'); const factory = require('factory'); const superTest = require('../utils/super_test'); const testEnvironment = require('../utils/test_environment_preparer'); const path = '/v1/admin/recipes'; test(`API GET ${path}`, co.wrap(function* (t) { yield testEnvironment.prepare(); const recipe1 = yield factory.create('recipe'); const recipe2 = yield factory.create('recipe'); const serverResponse = yield superTest.get(path); t.deepEqual(serverResponse.body, [recipe1, recipe2]); }));
上記のコードは、保存されたレシピの配列を応答として返すことを期待するAPI( GET /v1/admin/recipes
)をテストしています。
テストは、単純なものであっても、多くのユーティリティに依存していることがわかります。 これは、優れた統合テストスイートでは一般的です。
ヘルパーコンポーネントを使用すると、わかりやすい統合テストを簡単に作成できます。
統合テストに必要なコンポーネントを確認しましょう。
ヘルパーコンポーネント
包括的なテストスイートには、フロー制御、テストフレームワーク、データベースハンドラー、バックエンドAPIへの接続方法などの基本的な要素がいくつか含まれています。
フロー制御
JavaScriptテストの最大の課題の1つは、非同期フローです。
コールバックはコードに大混乱をもたらす可能性があり、約束だけでは不十分です。 ここでフローヘルパーが役立ちます。
async / awaitが完全にサポートされるのを待っている間、同様の動作のライブラリを使用できます。 目標は、非同期フローを持つ可能性のある、読みやすく、表現力豊かで、堅牢なコードを作成することです。
Coを使用すると、コードをブロックせずに、適切な方法で記述できます。 これは、コジェネレーター関数を定義し、結果を生成することによって行われます。
別の解決策は、Bluebirdを使用することです。 Bluebirdは、配列、エラー、時間などの処理などの非常に便利な機能を備えたPromiseライブラリです。
CoとBluebirdのコルーチンは、ES7のasync / awaitと同様に動作します(続行する前に解決を待機します)。唯一の違いは、エラーの処理に役立つpromiseを常に返すことです。
テストフレームワーク
テストフレームワークの選択は、個人的な好みに帰着します。 私の好みは、使いやすく、副作用がなく、出力が読みやすく、パイプ処理されやすいフレームワークです。
JavaScriptにはさまざまなテストフレームワークがあります。 この例では、テープを使用しています。 私の意見では、テープはこれらの要件を満たすだけでなく、モカやジャスミンのような他のテストフレームワークよりもクリーンでシンプルです。
テープは、Test Anything Protocol(TAP)に基づいています。
TAPには、ほとんどのプログラミング言語用のバリエーションがあります。
Tapeはテストを入力として受け取り、それらを実行してから、結果をTAPとして出力します。 次に、TAPの結果をテストレポーターにパイプするか、raw形式でコンソールに出力することができます。 テープはコマンドラインから実行されます。
Tapeには、テストスイート全体を実行する前にロードするモジュールの定義、小さくて単純なアサーションライブラリの提供、テストで呼び出す必要のあるアサーションの数の定義など、いくつかの優れた機能があります。 モジュールを使用してプリロードすると、テスト環境の準備が簡単になり、不要なコードを削除できます。
ファクトリーライブラリ
ファクトリライブラリを使用すると、静的フィクスチャファイルを、テスト用のデータを生成するためのはるかに柔軟な方法に置き換えることができます。 このようなライブラリを使用すると、面倒で複雑なコードを記述せずに、モデルを定義し、それらのモデルのエンティティを作成できます。

JavaScriptには、このためのfactory_girlがあります。これは、RubyonRails用に開発された同様の名前のgemから着想を得たライブラリです。
const factory = require('factory-girl').factory; const User = require('../models/user'); factory.define('user', User, { username: 'Bob', number_of_recipes: 50 }); const user = factory.build('user');
開始するには、factory_girlで新しいモデルを定義する必要があります。
これは、名前、プロジェクトのモデル、および新しいインスタンスが生成されるオブジェクトで指定されます。
または、新しいインスタンスの生成元となるオブジェクトを定義する代わりに、オブジェクトまたはPromiseを返す関数を提供することもできます。
モデルの新しいインスタンスを作成するとき、次のことができます。
- 新しく生成されたインスタンスの値をオーバーライドします
- ビルド関数オプションに追加の値を渡します
例を見てみましょう。
const factory = require('factory-girl').factory; const User = require('../models/user'); factory.define('user', User, (buildOptions) => { return { name: 'Mike', surname: 'Dow', email: buildOptions.email || '[email protected]' } }); const user1 = factory.build('user'); // {"name": "Mike", "surname": "Dow", "email": "[email protected]"} const user2 = factory.build('user', {name: 'John'}, {email: '[email protected]'}); // {"name": "John", "surname": "Dow", "email": "[email protected]"}
APIへの接続
本格的なHTTPサーバーを起動して実際のHTTPリクエストを作成し、数秒後にそれを破棄するだけで(特に複数のテストを実行する場合)、完全に非効率的であり、統合テストに必要以上に時間がかかる可能性があります。
SuperTestは、新しいアクティブサーバーを作成せずにAPIを呼び出すためのJavaScriptライブラリです。 これは、TCP要求を作成するためのライブラリであるSuperAgentに基づいています。 このライブラリを使用すると、新しいTCP接続を作成する必要はありません。 APIはほぼ瞬時に呼び出されます。
SuperTestは、promiseをサポートしており、promisedとしてのsupertestです。 このようなリクエストがpromiseを返すと、ネストされた複数のコールバック関数を回避できるため、フローの処理がはるかに簡単になります。
const express = require('express') const request = require('supertest-as-promised'); const app = express(); request(app).get("/recipes").then(res => assert(....));
SuperTestはExpress.jsフレームワーク用に作成されましたが、小さな変更を加えるだけで、他のフレームワークでも使用できます。
その他のユーティリティ
場合によっては、コードの依存関係をモックしたり、スパイを使用して関数のロジックをテストしたり、特定の場所でスタブを使用したりする必要があります。 ここで、これらのユーティリティパッケージのいくつかが役に立ちます。
SinonJSは、テスト用のスパイ、スタブ、モックをサポートする優れたライブラリです。 また、曲げ時間、テストサンドボックス、拡張アサーション、偽のサーバーやリクエストなど、他の便利なテスト機能もサポートしています。
場合によっては、コードに依存関係をモックする必要があります。 モックしたいサービスへの参照は、システムの他の部分で使用されます。
この問題を解決するには、依存性注入を使用するか、それがオプションでない場合は、Mockeryのようなモックサービスを使用できます。
Mockeryは、外部依存関係を持つコードをモックするのに役立ちます。 正しく使用するには、テストまたはコードをロードする前にMockeryを呼び出す必要があります。
const mockery = require('mockery'); mockery.enable({ warnOnReplace: false, warnOnUnregistered: false }); const mockingStripe = require('lib/services/internal/stripe'); mockery.registerMock('lib/services/internal/stripe', mockingStripe);
この新しいリファレンス(この例ではmockingStripe
)を使用すると、テストの後半でサービスをモックするのが簡単になります。
const stubStripeTransfer = sinon.stub(mockingStripe, 'transferAmount'); stubStripeTransfer.returns(Promise.resolve(null));
Sinonライブラリの助けを借りて、それは簡単にモックできます。 ここでの唯一の問題は、このスタブが他のテストに伝播することです。 サンドボックス化するには、sinonサンドボックスを使用できます。 これを使用すると、後のテストでシステムを初期状態に戻すことができます。
const sandbox = require('sinon').sandbox.create(); const stubStripeTransfer = sandbox.sinon.stub(mockingStripe, 'transferAmount'); stubStripeTransfer.returns(Promise.resolve(null)); // after the test, or better when starting a new test sandbox.restore();
次のような機能のために他のコンポーネントが必要です。
- データベースを空にする(1つの階層のビルド前クエリで実行できます)
- 動作状態に設定する(sequelize-fixtures)
- サードパーティサービスへのTCP要求のモック(ノック)
- より豊富なアサーションの使用(chai)
- サードパーティからの保存された応答(簡単な修正)
それほど単純ではないテスト
抽象化と拡張性は、効果的な統合テストスイートを構築するための重要な要素です。 テストのコアからフォーカスを取り除くすべて(データの準備、アクション、およびアサーション)は、グループ化してユーティリティ関数に抽象化する必要があります。
ここには正しい道も間違った道もありませんが、すべてがプロジェクトとそのニーズに依存するため、いくつかの重要な品質は、優れた統合テストスイートに共通しています。
次のコードは、レシピを作成し、副作用としてメールを送信するAPIをテストする方法を示しています。
外部の電子メールプロバイダーをスタブ化して、実際に電子メールを送信せずに電子メールが送信されたかどうかをテストできるようにします。 このテストでは、APIが適切なステータスコードで応答したかどうかも確認します。
const co = require('co'); const factory = require('factory'); const superTest = require('../utils/super_test'); const basicEnv = require('../utils/basic_test_enivornment'); const path = '/v1/admin/recipes'; basicEnv.test(`API POST ${path}`, co.wrap(function* (t, assert, sandbox) { const chef = yield factory.create('chef'); const body = { chef_id: chef.id, recipe_name: 'cake', Ingredients: ['carrot', 'chocolate', 'biscuit'] }; const stub = sandbox.stub(mockery.emailProvider, 'sendNewEmail').returnsPromise(null); const serverResponse = yield superTest.get(path, body); assert.spies(stub).called(1); assert.statusCode(serverResponse, 201); }));
上記のテストは、毎回クリーンな環境で開始されるため、繰り返し可能です。
セットアップに関連するすべてがbasicEnv.test
関数内に統合される、シンプルなセットアッププロセスがあります。
1つのアクション(単一のAPI)のみをテストします。 また、単純なassertステートメントを通じて、テストの期待を明確に示しています。 また、テストには、スタブ/モックによるサードパーティのコードは含まれていません。
統合テストの作成を開始します
新しいコードを本番環境にプッシュする場合、開発者(および他のすべてのプロジェクト参加者)は、新しい機能が機能し、古い機能が壊れないことを確認したいと考えています。
これは、テストなしで達成するのは非常に困難であり、不十分に行われると、フラストレーション、プロジェクトの疲労、そして最終的にはプロジェクトの失敗につながる可能性があります。
統合テストは、単体テストと組み合わせて、防御の最前線です。
2つのうち1つだけを使用するだけでは不十分であり、カバーされていないエラーのために多くのスペースが残ります。 常に両方を利用することで、新しいコミットが堅牢になり、すべてのプロジェクト参加者に信頼をもたらし、信頼を得ることができます。