次の方法で共有


JavaScript アプリケーションで Azure SDK 統合をテストする方法

アプリケーションが Azure サービスと正しく対話できるようにするには、Azure SDK for JavaScript の統合コードのテストが不可欠です。 このガイドでは、JavaScript アプリケーションでテスト フレームワークで Azure SDK 統合を効果的にテストする方法について説明します。

クラウド サービス SDK の呼び出しをモックアウトするか、テスト目的でライブ サービスを使用するかを決定するときは、速度、信頼性、コストのトレードオフを考慮することが重要です。 この記事では、SDK 統合をテストするためにテスト フレームワークを使用する方法について説明します。 アプリケーション コードは、Cosmos DB にドキュメントを挿入します。 テスト コードでは、クラウド リソースが使用されないように、そのリソースの使用状況がモックアウトされます。

使用されるフレームワークは次のとおりです。

  • Jest と CommonJs
  • ESM を使用した Vitest
  • ESM を使用するテストランナー Node.js

前提条件

Node.js LTS。 LTS リリースの状態 は "長期的なサポート" であり、通常、重大なバグが合計 30 か月間修正されることを保証します。

Node.js テスト ランナーは、Node.js インストールの一部です。

注意事項

Node.js テスト ランナー用に提供されるサンプルでは、mock.fn() を含む実験用 node:test モジュールを使用します。 Node の組み込みのテスト ランナーには、完全にサポートされているモック API がまだ用意されていないことに注意してください。 ターゲットの Node バージョンで実験用 API がサポートされていることを確認するか、代わりにサードパーティ製のモック ライブラリ (またはスタブ関数) の使用を検討してください。

クラウド サービスのモッキング

長所:

  • ネットワーク待ち時間を排除することで、テスト スイートを高速化します。
  • 予測可能で制御されたテスト環境を提供します。
  • さまざまなシナリオやエッジ ケースを簡単にシミュレートできます。
  • ライブ クラウド サービス (特に継続的インテグレーション パイプライン) の使用に関連するコストを削減します。

短所:

  • モックは実際の SDK からずれることがあり、不一致につながる可能性があります。
  • ライブ サービスの特定の機能や動作を無視する場合があります。
  • 運用環境に比べて現実的でない環境。

ライブ サービスの使用

長所:

  • 生産を密接に反映する現実的な環境ですか?
  • システムのさまざまな部分が連携するように統合テストに役立ちますか?
  • ネットワークの信頼性、サービスの可用性、および実際のデータ処理に関連する問題を特定するのに役立ちますか?

短所:

  • ネットワーク呼び出しが原因で低速になります。
  • サービスの使用コストが発生する可能性があるため、コストが高くなります。
  • 運用環境に一致するライブ サービス環境をセットアップして維持するには、複雑で時間がかかります。

ライブ サービスのモッキングと使用の選択は、テスト戦略によって異なります。 速度と制御が最も重要な単体テストでは、多くの場合、モッキングが適しています。 現実感が重要な統合テストでは、ライブ サービスを使用すると、より正確な結果が得られます。 これらのアプローチを分散することで、コストを管理し、テスト効率を維持しながら、包括的なテスト カバレッジを実現できます。

テスト ダブル: モック、スタブ、フェイク

テスト ダブルは、テスト目的で実際の何かの代わりに使用される任意の種類の代替です。 選択するダブルの種類は、置き換える内容に基づいています。 モックという用語は、多くの場合、この用語がカジュアルに使用される場合は、ダブルという意味で使用されます。 この記事では、この用語を Jest テスト フレームワークで具体的に使用し、具体的に説明します。

モック

モック (スパイとも呼ばれる): 関数を置き換え、他のコードによって間接的に呼び出されたときに、その関数の動作を制御しスパイできます。

次の例では、2 つの関数があります。

  • someTestFunction: テストする必要がある関数。 dependencyFunction の依存関係を呼び出します。これは、記述されておらず、テストする必要はありません。
  • dependencyFunctionMock: 依存関係のモック。
import { mock } from 'node:test';
import assert from 'node:assert';

// ARRANGE
const dependencyFunctionMock = mock.fn();

// ACT
// Mock replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()

// ASSERT
assert.strictEqual(dependencyFunctionMock.mock.callCount(), 1);

テストの目的は、依存関係コードを実際に呼び出すことなく、someTestFunction が正しく動作することを確認することです。 テストでは、依存関係のモックが呼び出されたことを検証します。

大きな依存関係と小さい依存関係をモックする

依存関係をモックする場合は、次のように必要なものだけをモックすることを選択できます。

  • 大きな依存関係からの 1 つまたは 2 つの関数。 Jest には、この目的のためにパーシャルモックがあります。
  • この記事の例に示すように、小さい方の依存関係のすべての関数。

スタブ

スタブの目的は、関数の戻り値データを置き換えて、さまざまなシナリオをシミュレートすることです。 スタブを使用して、コードで関数を呼び出し、成功した結果、失敗、例外、エッジ ケースなど、さまざまな状態を受け取れるようにします。 状態検証 は、これらのシナリオをコードが正しく処理することを確認します。

import { describe, it, beforeEach, mock } from 'node:test';
import assert from 'node:assert';

// ARRANGE
const fakeDatabaseData = {first: 'John', last: 'Jones'};

const dependencyFunctionMock = mock.fn();
dependencyFunctionMock.mock.mockImplementation((arg) => {
    return fakeDatabaseData;
});

// ACT
// Mock replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()

// ASSERT
assert.strictEqual(name, `${fakeDatabaseData.first} ${fakeDatabaseData.last}`);

前のテストの目的は、someTestFunction によって実行された作業が期待される結果を満たしていることを確認することです。 この簡単な例では、関数のタスクは、氏名を連結することです。 偽のデータを使用すると、期待される結果を把握し、関数が正しく動作することを検証できます。

偽物

偽物は、クラウド データベースではなくメモリ内データベースを使用するなど、運用環境では通常使用しない機能に代わるものです。

// fake-in-mem-db.spec.ts
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert';

class FakeDatabase {
  private data: Record<string, any>;

  constructor() {
    this.data = {};
  }

  save(key: string, value: any): void {
    this.data[key] = value;
  }

  get(key: string): any {
    return this.data[key];
  }
}

// Function to test
function someTestFunction(db: FakeDatabase, key: string, value: any): any {
  db.save(key, value);
  return db.get(key);
}

describe('In-Mem DB', () => {
  let fakeDb: FakeDatabase;
  let testKey: string;
  let testValue: any;

  beforeEach(() => {
    fakeDb = new FakeDatabase();
    testKey = 'testKey';
    testValue = {
      first: 'John',
      last: 'Jones',
      lastUpdated: new Date().toISOString(),
    };
  });

  afterEach(() => {
    // Restore all mocks created by node:test’s mock helper.
    mock.restoreAll();
  });

  it('should save and return the correct value', () => {
    // Create a spy on the save method using node:test's mock helper.
    const saveSpy = mock.method(fakeDb, 'save').mock;

    // Call the function under test.
    const result = someTestFunction(fakeDb, testKey, testValue);

    // Verify state.
    assert.deepStrictEqual(result, testValue);
    assert.strictEqual(result.first, 'John');
    assert.strictEqual(result.last, 'Jones');
    assert.strictEqual(result.lastUpdated, testValue.lastUpdated);

    // Verify behavior
    assert.strictEqual(saveSpy.callCount(), 1);
    const calls = saveSpy.calls;
    assert.deepStrictEqual(calls[0].arguments, [testKey, testValue]);
  });
});

前のテストの目的は、someTestFunction がデータベースと正しく対話することを確認することです。 偽のメモリ内データベースを使用すると、実際のデータベースに依存することなく関数のロジックをテストでき、テストの高速化と信頼性が向上します。

シナリオ: Azure SDK を使用して Cosmos DB にドキュメントを挿入する

すべての情報が送信されて検証される場合に、Cosmos DB に新しいドキュメントを書き込む必要があるアプリケーションがあるとします。 空のフォームが送信された場合、または情報が予想される形式と一致しない場合、アプリケーションはデータを入力するべきではありません。

Cosmos DB は例として使用されますが、概念はほとんどの JavaScript 用 Azure SDK に適用されます。 次の関数は、この機能をキャプチャします。

// insertDocument.ts
import { Container } from '../data/connect-to-cosmos.js';
import type {
  DbDocument,
  DbError,
  RawInput,
  VerificationErrors,
} from '../data/model.js';
import Verify from '../data/verify.js';

export async function insertDocument(
  container: Container,
  doc: RawInput,
): Promise<DbDocument | DbError | VerificationErrors> {
  const isVerified: boolean = Verify.inputVerified(doc);

  if (!isVerified) {
    return { message: 'Verification failed' } as VerificationErrors;
  }

  try {
    const { resource } = await container.items.create({
      id: doc.id,
      name: `${doc.first} ${doc.last}`,
    });

    return resource as DbDocument;
  } catch (error: any) {
    if (error instanceof Error) {
      if ((error as any).code === 409) {
        return {
          message: 'Insertion failed: Duplicate entry',
          code: 409,
        } as DbError;
      }
      return { message: error.message, code: (error as any).code } as DbError;
    } else {
      return { message: 'An unknown error occurred', code: 500 } as DbError;
    }
  }
}

TypeScript 型は、関数が使用するデータの種類を定義するのに役立ちます。 Jest やその他の JavaScript テスト フレームワークを使用するために TypeScript は必要ありませんが、型セーフな JavaScript を記述するために不可欠です。

このアプリケーションの関数は次のとおりです。

関数 説明
insertDocument ドキュメントをデータベースに挿入します。 これはテスト対象です
inputVerified スキーマに対して入力データを検証します。 データが正しい形式 (有効なメール アドレス、正しく書式設定された URL など) であることを確認します。
cosmos.items.create @azure/cosmos を使用する Azure Cosmos DB の SDK 関数。 これはモック対象です。 パッケージ所有者によって管理されている独自のテストが既に存在します。 受信データが検証に合格した場合、Cosmos DB 関数呼び出しが行われ、データが返されたことを確認する必要があります。

テスト フレームワークの依存関係をインストールする

このフレームワークは、Node.js LTS の一部として提供されます。

テストを実行するようにパッケージを構成する

アプリケーション用の package.json を新しいスクリプトを使用して更新し、ソース コード ファイルをテストします。 ソース コード ファイルは、ファイル名と拡張子の一部を照合することによって定義されます。 テスト ランナーは、テスト ファイルの一般的な名前付け規則 ( <file-name>.spec.[jt]s) に従ってファイルを検索します。 このパターンは、次の例のような名前のファイルがテスト ファイルとして解釈され、テスト ランナーによって実行されていることを意味します。

  • * .test.js: たとえば、math.test.js
  • * .spec.js: たとえば、math.spec.js
  • tests/math.js など tests directory にあるファイル

テスト ランナーを使用してそのテスト ファイル パターンをサポートするスクリプトを package.json に追加します。

"scripts": {
    "test": "node --test --experimental-test-coverage --experimental-test-module-mocks --trace-exit"
}

Azure SDK の単体テストを設定する

モック、スタブ、およびフェイクを使用して、insertDocument 関数をテストするにはどうすればよいですか?

  • モック: 関数の動作が次のようにテストされていることを確認するためにモックが必要です。
    • データが検証に合格した場合、Cosmos DB 関数の呼び出しは 1 回だけ発生しました
    • データが検証に合格しない場合、Cosmos DB 関数の呼び出しは行われませんでした
  • スタブ:
    • 合格したデータは、関数によって返される新しいドキュメントと一致します。

テストするときは、テストのセットアップ、テスト自体、検証の観点から考えてください。 テスト用語の観点から、この機能は次の用語を使用します。

  • 配置: テスト条件を設定する
  • 実行: system under test または SUT と呼ばれるテストする関数を呼び出します。
  • 断言: 結果を検証します。 結果には、動作または状態を指定できます。
    • 動作は、検証できるテスト関数の機能を示します。 1 つの例として、依存関係の呼び出しが挙げられます。
    • 状態は、関数から返されたデータを示します。
import { describe, it, afterEach, beforeEach, mock } from 'node:test';
import assert from 'node:assert';

describe('boilerplate', () => {
  beforeEach(() => {
    // Setup required before each test
  });
  afterEach(() => {
    // Cleanup required after each test
  });

  it('should <do something> if <situation is present>', async () => {
    // Arrange
    // - set up the test data and the expected result
    // Act
    // - call the function to test
    // Assert
    // - check the state: result returned from function
    // - check the behavior: dependency function calls
  });
});

テストでモックを使用する場合、そのテンプレート コードでは、Azure クライアント ライブラリなど、関数で使用される基になる依存関係を呼び出さずに、モックを使用して関数をテストする必要があります。

テスト ファイルを作成する

依存関係の呼び出しをシミュレートするためのモックを含むテスト ファイルには、追加のセットアップがあります。

テスト ファイルにはいくつかの部分があります。

  • import: import ステートメントを使用すると、任意のテストを使用したりモックアウトしたりできます。
  • mock: 目的の既定のモック動作を作成します。 各テストは、必要に応じて変更できます。
  • describe: insert.ts ファイルのグループ ファミリをテストします。
  • it: insert.ts ファイルの各テスト。

テスト ファイルには、insert.ts ファイルの 3 つのテストが含まれています。これは、2 つの検証の種類に分けることができます。

検証タイプ テスト
ハッピー パス: should insert document successfully モック化されたデータベース メソッドが呼び出され、変更されたデータが返されました。
エラー パス: should return verification error if input is not verified データの検証に失敗し、エラーが返されました。
エラー パス:should return error if db insert fails モック化されたデータベース メソッドが呼び出され、エラーが返されました。

次のテスト ファイルは、 insertDocument 関数をテストする方法を示しています。

// insertDocument.test.ts
import { describe, it, beforeEach, mock } from 'node:test';
import assert from 'node:assert';

import { Container } from '../src/data/connect-to-cosmos.js';
import { createTestInputAndResult } from '../src/data/fake-data.js';
import type { DbDocument, DbError, RawInput } from '../src/data/model.js';
import { isDbError, isVerificationErrors } from '../src/data/model.js';

import Verify from '../src/data/verify.js';
import CosmosConnector from '../src/data/connect-to-cosmos.js';
import { insertDocument } from '../src/lib/insert.js';

describe('SDK', () => {
  beforeEach(() => {
    // Clear all mocks before each test
    mock.restoreAll();
  });

  it('should return verification error if input is not verified', async () => {
    const fakeContainer = {
      items: {
        create: async (_: any) => {
          throw new Error('Create method not implemented');
        },
      },
    } as unknown as Container;

    const mVerify = mock.method(Verify, 'inputVerified').mock;
    mVerify.mockImplementation(() => false);

    const mGetUniqueId = mock.method(CosmosConnector, 'getUniqueId').mock;
    mGetUniqueId.mockImplementation(() => 'unique-id');

    const mContainerCreate = mock.method(fakeContainer.items, 'create').mock;

    // Arrange: wrong shape of document on purpose.
    const doc = { name: 'test' } as unknown as RawInput;

    // Act:
    const insertDocumentResult = await insertDocument(fakeContainer, doc);

    // Assert - State verification.
    if (isVerificationErrors(insertDocumentResult)) {
      assert.deepStrictEqual(insertDocumentResult, {
        message: 'Verification failed',
      });
    } else {
      throw new Error('Result is not of type VerificationErrors');
    }

    // Assert - Behavior verification: Verify that create was never called.
    assert.strictEqual(mContainerCreate.callCount(), 0);
  });
  it('should insert document successfully', async () => {
    // Arrange: override inputVerified to return true.
    const { input, result }: { input: RawInput; result: Partial<DbDocument> } =
      createTestInputAndResult();

    const fakeContainer = {
      items: {
        create: async (doc: any) => {
          return { resource: result };
        },
      },
    } as unknown as Container;

    const mVerify = mock.method(Verify, 'inputVerified').mock;
    mVerify.mockImplementation(() => true);

    const mContainerCreate = mock.method(
      fakeContainer.items as any,
      'create',
    ).mock;
    mContainerCreate.mockImplementation(async (doc: any) => {
      return { resource: result };
    });

    // Act:
    const receivedResult = await insertDocument(fakeContainer, input);

    // Assert - State verification: Ensure the result is as expected.
    assert.deepStrictEqual(receivedResult, result);

    // Assert - Behavior verification: Ensure create was called once with correct arguments.
    assert.strictEqual(mContainerCreate.callCount(), 1);
    assert.deepStrictEqual(mContainerCreate.calls[0].arguments[0], {
      id: input.id,
      name: result.name,
    });
  });
  it('should return error if db insert fails', async () => {
    // Arrange: override inputVerified to return true.
    const { input, result } = createTestInputAndResult();
    const errorMessage: string = 'An unknown error occurred';

    const fakeContainer = {
      items: {
        create: async (doc: any): Promise<any> => {
          return Promise.resolve(null);
        },
      },
    } as unknown as Container;

    const mVerify = mock.method(Verify, 'inputVerified').mock;
    mVerify.mockImplementation(() => true);

    const mContainerCreate = mock.method(fakeContainer.items, 'create').mock;
    mContainerCreate.mockImplementation(async (doc: any) => {
      const mockError: DbError = {
        message: errorMessage,
        code: 500,
      };
      throw mockError;
    });

    // Act:
    const insertDocumentResult = await insertDocument(fakeContainer, input);

    // // Assert - Ensure create method was called once with the correct arguments.
    assert.strictEqual(isDbError(insertDocumentResult), true);
    assert.strictEqual(mContainerCreate.callCount(), 1);
    assert.deepStrictEqual(mContainerCreate.calls[0].arguments[0], {
      id: input.id,
      name: result.name,
    });
  });
});

トラブルシューティング

この記事のコードのほとんどは、 MicrosoftDocs/node-essentials GitHub リポジトリから取得されています。 Cosmos DB Cloud リソースに挿入する場合は、 このスクリプトを使用してリソースを作成します

追加情報