JavaScript アプリケーションでの Azure SDK 統合のテスト
アプリケーションが Azure サービスと正しく対話できるようにするには、Azure SDK for JavaScript の統合コードのテストが不可欠です。
クラウド サービス SDK の呼び出しをモックアウトするか、テスト目的でライブ サービスを使用するかを決定するときは、速度、信頼性、コストのトレードオフを考慮することが重要です。
前提条件
クラウド サービスのモッキング
長所:
- ネットワーク待ち時間を排除することで、テスト スイートを高速化します。
- 予測可能で制御されたテスト環境を提供します。
- さまざまなシナリオやエッジ ケースを簡単にシミュレートできます。
- ライブ クラウド サービス (特に継続的インテグレーション パイプライン) の使用に関連するコストを削減します。
短所:
- モックは実際の SDK からドリフトが生じ、不一致が発生する可能性があります。
- ライブ サービスの特定の機能や動作を無視する場合があります。
- 運用環境に比べて現実的でない環境。
ライブ サービスの使用
長所:
- 運用環境を厳密に反映する現実的な環境を提供します。
- システムのさまざまな部分が連携していることを確認するための統合テストに役立ちます。
- ネットワークの信頼性、サービスの可用性、および実際のデータ処理に関連する問題を特定するのに役立ちます。
短所:
- ネットワーク呼び出しが原因で低速になります。
- サービスの使用コストが発生する可能性があるため、コストが高くなります。
- 運用環境に一致するライブ サービス環境をセットアップして維持するには、複雑で時間がかかります。
ライブ サービスのモッキングと使用の選択は、テスト戦略によって異なります。 速度と制御が最も重要な単体テストでは、多くの場合、モッキングが適しています。 現実感が重要な統合テストでは、ライブ サービスを使用すると、より正確な結果が得られます。 これらのアプローチを分散することで、コストを管理し、テスト効率を維持しながら、包括的なテスト カバレッジを実現できます。
テスト ダブル: モック、スタブ、フェイク
テスト ダブルは、テスト目的で実際の何かの代わりに使用される任意の種類の代替です。 選択するダブルの種類は、置き換える内容に基づいています。 モックという用語は、多くの場合、この用語がカジュアルに使用される場合は、ダブルという意味で使用されます。 この記事では、この用語を Jest テスト フレームワークで具体的に使用し、具体的に説明します。
モック
モック (スパイとも呼ばれる): 関数を置き換え、他のコードによって間接的に呼び出されたときに、その関数の動作を制御しスパイできます。
次の例では、2 つの関数があります。
- someTestFunction: これはテストする必要がある関数です。
dependencyFunction
の依存関係を呼び出します。これは、記述されておらず、テストする必要はありません。 - dependencyFunctionMock: これは依存関係のモックです。
// setup
const dependencyFunctionMock = jest.fn();
// perform test
// Jest replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()
// verify behavior
expect(dependencyFunctionMock).toHaveBeenCalled();
テストの目的は、依存関係コードを実際に呼び出すことなく、someTestFunction が正しく動作することを確認することです。 テストでは、依存関係のモックが呼び出されたことを検証します。
大きな依存関係と小さい依存関係をモックする
依存関係をモックする場合は、次のように必要なものだけをモックすることを選択できます。
- 大きな依存関係からの 1 つまたは 2 つの関数。 Jest には、この目的のためにパーシャルモックがあります。
- この記事の例に示すように、小さい方の依存関係のすべての関数。
スタブ
スタブの目的は、関数の戻り値データを置き換えて、さまざまなシナリオをシミュレートすることです。 これにより、コードで関数を呼び出し、成功した結果、失敗、例外、エッジ ケースなど、さまざまな状態を受け取ることができます。 状態検証 は、これらのシナリオをコードが正しく処理することを確認します。
// ARRANGE
const dependencyFunctionMock = jest.fn();
const fakeDatabaseData = {first: 'John', last: 'Jones'};
dependencyFunctionMock.mockReturnValue(fakeDatabaseData);
// ACT
// date is returned by mock then transformed in SomeTestFunction()
const { name } = someTestFunction()
// ASSERT
expect(name).toBe(`${first} ${last}`);
前のテストの目的は、someTestFunction
によって実行された作業が期待される結果を満たしていることを確認することです。 この簡単な例では、関数のタスクは、氏名を連結することです。 偽のデータを使用すると、期待される結果を把握し、関数が正しく動作することを検証できます。
Fakes
偽物は、クラウド データベースではなくメモリ内データベースを使用するなど、運用環境では通常使用しない機能に代わるものです。
// fake-in-mem-db.spec.ts
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);
}
// Jest test suite
describe('someTestFunction', () => {
let fakeDb: FakeDatabase;
let testKey: string;
let testValue: any;
beforeEach(() => {
fakeDb = new FakeDatabase();
testKey = 'testKey';
testValue = {
first: 'John',
last: 'Jones',
lastUpdated: new Date().toISOString(),
};
// Spy on the save method
jest.spyOn(fakeDb, 'save');
});
afterEach(() => {
// Clear all mocks
jest.clearAllMocks();
});
test('should save and return the correct value', () => {
// Perform test
const result = someTestFunction(fakeDb, testKey, testValue);
// Verify state
expect(result).toEqual(testValue);
expect(result.first).toBe('John');
expect(result.last).toBe('Jones');
expect(result.lastUpdated).toBe(testValue.lastUpdated);
// Verify behavior
expect(fakeDb.save).toHaveBeenCalledWith(testKey, testValue);
});
});
前のテストの目的は、someTestFunction
がデータベースと正しく対話することを確認することです。 偽のメモリ内データベースを使用すると、実際のデータベースに依存することなく関数のロジックをテストでき、テストの高速化と信頼性が向上します。
シナリオ: Azure SDK を使用して Cosmos DB にドキュメントを挿入する
すべての情報が送信されて検証される場合に、Cosmos DB に新しいドキュメントを書き込む必要があるアプリケーションがあるとします。 空のフォームが送信された場合、または情報が予想される形式と一致しない場合、アプリケーションはデータを入力するべきではありません。
Cosmos DB は例として使用されますが、概念はほとんどの JavaScript 用 Azure SDK に適用されます。 次の関数は、この機能をキャプチャします。
// insertDocument.ts
import { Container } from '../data/connect-to-cosmos';
import {
DbDocument,
DbError,
RawInput,
VerificationErrors,
} from '../data/model';
import { inputVerified } from '../data/verify';
export async function insertDocument(
container: Container,
doc: RawInput,
): Promise<DbDocument | DbError | VerificationErrors> {
const isVerified: boolean = 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;
}
}
}
Note
TypeScript 型は、関数が使用するデータの種類を定義するのに役立ちます。 Jest やその他の JavaScript テスト フレームワークを使用するために TypeScript は必要ありませんが、タイプセーフ JavaScript を記述するために不可欠です。
上記のアプリケーションの関数は次のとおりです。
関数 | 説明 |
---|---|
insertDocument | ドキュメントをデータベースに挿入します。 これはテスト対象です。 |
inputVerified | スキーマに対して入力データを検証します。 データが正しい形式 (有効なメール アドレス、正しく書式設定された URL など) であることを確認します。 |
cosmos.items.create | @azure/cosmos を使用する Azure Cosmos DB の SDK 関数。 これはモック対象です。 パッケージ所有者によって管理されている独自のテストが既に存在します。 受信データが検証に合格した場合、Cosmos DB 関数呼び出しが行われ、データが返されたことを確認する必要があります。 |
テスト フレームワークの依存関係をインストールする
この記事では、テスト フレームワークとして Jest を使用します。 他のテスト フレームワークもあります。これは、使用できる同等のフレームワークです。
アプリケーション ディレクトリの根底で、次のコマンドを使用して Jest をインストールします。
npm install jest
テストを実行するようにパッケージを構成する
アプリケーション用の package.json
を新しいスクリプトを使用して更新し、ソース コード ファイルをテストします。 ソース コード ファイルは、ファイル名と拡張子の一部を照合することによって定義されます。 Jest は、テスト ファイルの一般的な名前付け規則 ( <file-name>.spec.[jt]s
) に従ってファイルを探します。 このパターンは、次の例のような名前のファイルがテスト ファイルとして解釈され、Jest によって実行されることを意味します。
- *.test.js: たとえば、math.test.js
- *.spec.js: たとえば、math.spec.js
- tests/math.js など tests directory にあるファイル
Jest でテスト ファイル パターンをサポートするスクリプトを package.json に追加します。
"scripts": {
"test": "jest dist",
}
TypeScript ソース コードは、dist
サブフォルダーに生成されます。 Jest は、dist
サブフォルダーにある .spec.js
ファイルを実行します。
Azure SDK の単体テストを設定する
モック、スタブ、およびフェイクを使用して、insertDocument 関数をテストするにはどうすればよいですか?
- モック: 関数の動作が次のようにテストされていることを確認するためにモックが必要です。
- データが検証に合格した場合、Cosmos DB 関数の呼び出しは 1 回だけ発生しました
- データが検証に合格しない場合、Cosmos DB 関数の呼び出しは行われませんでした
- スタブ:
- 合格したデータは、関数によって返される新しいドキュメントと一致します。
テストするときは、テストのセットアップ、テスト自体、検証の観点から考えてください。 テスト用語の観点からは、これは次のように呼ばれます。
- 配置: テスト条件を設定する
- 実行: system under test または SUT と呼ばれるテストする関数を呼び出します。
- 断言: 結果を検証します。 結果には、動作または状態を指定できます。
- 動作は、検証できるテスト関数の機能を示します。 1 つの例として、依存関係の呼び出しが挙げられます。
- 状態は、関数から返されたデータを示します。
Jest には、他のテスト フレームワークと同様に、テスト ファイルを定義するためのテスト ファイルの定型句があります。
// boilerplate.spec.ts
describe('nameOfGroupOfTests', () => {
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 ステートメントを使用すると、任意のテストを使用したりモックアウトしたりできます。jest.mock
: 目的の既定のモック動作を作成します。 各テストは、必要に応じて変更できます。describe
:insert.ts
ファイルのグループ ファミリをテストします。test
: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 |
モック化されたデータベース メソッドが呼び出され、エラーが返されました。 |
次の Jest テスト ファイルは、insertDocument 関数をテストする方法を示しています。
// insertDocument.test.ts
import { Container } from '../data/connect-to-cosmos';
import { createTestInputAndResult } from '../data/fake-data';
import {
DbDocument,
DbError,
isDbError,
isVerificationErrors,
RawInput,
} from '../data/model';
import { inputVerified } from '../data/verify';
import { insertDocument } from './insert';
// Mock app dependencies for Cosmos DB setup
jest.mock('../data/connect-to-cosmos', () => ({
connectToContainer: jest.fn(),
getUniqueId: jest.fn().mockReturnValue('unique-id'),
}));
// Mock app dependencies for input verification
jest.mock('../data/verify', () => ({
inputVerified: jest.fn(),
}));
describe('insertDocument', () => {
// Mock the Cosmo DB Container object
let mockContainer: jest.Mocked<Container>;
beforeEach(() => {
// Clear all mocks before each test
jest.clearAllMocks();
// Mock the Cosmos DB Container create method
mockContainer = {
items: {
create: jest.fn(),
},
} as unknown as jest.Mocked<Container>;
});
it('should return verification error if input is not verified', async () => {
// Arrange - Mock the input verification function to return false
jest.mocked(inputVerified).mockReturnValue(false);
// Arrange - wrong shape of doc on purpose
const doc = { name: 'test' };
// Act - Call the function to test
const insertDocumentResult = await insertDocument(
mockContainer,
doc as unknown as RawInput,
);
// Assert - State verification: Check the result when verification fails
if (isVerificationErrors(insertDocumentResult)) {
expect(insertDocumentResult).toEqual({
message: 'Verification failed',
});
} else {
throw new Error('Result is not of type VerificationErrors');
}
// Assert - Behavior verification: Ensure create method was not called
expect(mockContainer.items.create).not.toHaveBeenCalled();
});
it('should insert document successfully', async () => {
// Arrange - create input and expected result data
const { input, result }: { input: RawInput; result: Partial<DbDocument> } =
createTestInputAndResult();
// Arrange - mock the input verification function to return true
(inputVerified as jest.Mock).mockReturnValue(true);
(mockContainer.items.create as jest.Mock).mockResolvedValue({
resource: result,
});
// Act - Call the function to test
const insertDocumentResult = await insertDocument(mockContainer, input);
// Assert - State verification: Check the result when insertion is successful
expect(insertDocumentResult).toEqual(result);
// Assert - Behavior verification: Ensure create method was called with correct arguments
expect(mockContainer.items.create).toHaveBeenCalledTimes(1);
expect(mockContainer.items.create).toHaveBeenCalledWith({
id: input.id,
name: result.name,
});
});
it('should return error if db insert fails', async () => {
// Arrange - create input and expected result data
const { input, result } = createTestInputAndResult();
// Arrange - mock the input verification function to return true
jest.mocked(inputVerified).mockReturnValue(true);
// Arrange - mock the Cosmos DB create method to throw an error
const mockError: DbError = {
message: 'An unknown error occurred',
code: 500,
};
jest.mocked(mockContainer.items.create).mockRejectedValue(mockError);
// Act - Call the function to test
const insertDocumentResult = await insertDocument(mockContainer, input);
// Assert - verify type as DbError
if (isDbError(insertDocumentResult)) {
expect(insertDocumentResult.message).toBe(mockError.message);
} else {
throw new Error('Result is not of type DbError');
}
// Assert - Behavior verification: Ensure create method was called with correct arguments
expect(mockContainer.items.create).toHaveBeenCalledTimes(1);
expect(mockContainer.items.create).toHaveBeenCalledWith({
id: input.id,
name: result.name,
});
});
});
追加情報
- Jest モックのベスト プラクティス
- Martin Fowler によるモックとスタブの違い