針對適用於 JavaScript 的 Azure SDK 測試整合程式碼,是確保應用程式與 Azure 服務正確互動的必要條件。 本指南說明如何在 JavaScript 應用程式中有效地測試 Azure SDK 整合測試架構。
在決定是否模擬雲端服務 SDK 呼叫或使用即時服務進行測試時,請務必考慮速度、可靠性和成本之間的取捨。 本文示範如何使用測試架構來測試 SDK 整合。 應用程式程式代碼會將檔插入Cosmos DB。 測試程式代碼會模擬該資源使用量,因此不會使用雲端資源。
使用的架構如下:
- Jest with CommonJs
- 使用 ESM 的 Vitest
- 使用 ESM 的測試運行器 Node.js
必要條件
Node.js LTS。 LTS 發行狀態為「長期支援」,通常保證修復重大 Bug,為期長達總共 30 個月。
Node.js 測試執行器是安裝 Node.js 的一部分。
謹慎
為 Node.js 測試執行器提供的範例會使用實驗 node:test 模組搭配 mock.fn()。 請記住,Node 的內建測試執行器尚未提供完全支援的模擬 API。 請確定您的目標 Node 版本支援實驗性 API,或考慮改用第三方模擬庫(或使用存根函式)。
模擬雲端服務
優點:
- 藉由消除網路等待時間來加速測試套件。
- 提供可預測且受控制的測試環境。
- 更容易模擬各種案例和邊緣案例。
- 降低與使用即時雲端服務相關的成本,特別是在持續整合管線中。
缺點:
- 模擬可能會與實際的 SDK 存在偏差,導致不一致。
- 可能會忽略即時服務的特定功能或行為。
- 與生產環境相比,這個環境顯得不夠現實。
使用即時服務
優點:
- 逼真的環境是否密切地反映生產環境?
- 整合測試是否適合確保系統的不同部分共同運作?
- 是否有助於識別與網路可靠性、服務可用性和實際數據處理相關的問題?
缺點:
- 由於網路呼叫而變慢。
- 由於潛在的服務使用成本,價格較高。
- 設定和維護符合生產環境的即時服務環境相當複雜且耗時。
模擬和使用實時服務之間的選擇取決於您的測試策略。 對於速度和控制至關重要的單元測試,模擬通常是更好的選擇。 對於現實主義至關重要的整合測試,使用即時服務可以提供更精確的結果。 平衡這些方法有助於達到完整的測試涵蓋範圍,同時管理成本和維護測試效率。
測試替身:模擬、存根和假件
測試替身是用於取代真實物來進行測試的任何替代物。 您選擇的雙類型是根據您想要取代的。 當字詞被隨意使用時,mock通常表示任何雙重。 在本文中,該詞彙會特別使用,並特別說明在 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);
// ARRANGE
const dependencyFunctionMock = jest.fn();
// ACT
// Jest replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()
// ASSERT
expect(dependencyFunctionMock).toHaveBeenCalled();
import { expect, vi } from 'vitest';
// ARRANGE
const dependencyFunctionMock = vi.fn();
// ACT
// Mock replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()
// ASSERT
expect(dependencyFunctionMock).toHaveBeenCalledTimes(1);
測試的目的是要確保 someTestFunction 的行為正確,而不會實際叫用相依性程序代碼。 測試會驗證呼叫相依性模擬。
模擬大型與小型相依性
當您決定模擬依賴項時,您可以選擇僅模擬所需的內容,例如:
- 從較大相依性中提取的一或兩個函式。 Jest 為此 提供部分模擬 。
- 較小依賴的所有函式,如本文範例所示。
Stub
存根的目的是要替換函數的回傳數據,以模擬不同的情境。 您可以使用存根來允許程式代碼呼叫函式並接收各種狀態,包括成功結果、失敗、例外狀況和邊緣案例。
狀態驗證 可確保您的程式代碼正確處理這些案例。
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}`);
// 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(`${fakeDatabaseData.first} ${fakeDatabaseData.last}`);
import { it, expect, vi } from 'vitest';
// ARRANGE
const fakeDatabaseData = {first: 'John', last: 'Jones'};
const dependencyFunctionMock = vi.fn();
dependencyFunctionMock.mockReturnValue(fakeDatabaseData);
// ACT
// date is returned by mock then transformed in SomeTestFunction()
const { name } = someTestFunction()
// ASSERT
expect(name).toBe(`${fakeDatabaseData.first} ${fakeDatabaseData.last}`);
上述測試的目的是要確保完成 someTestFunction 的工作符合預期的結果。 在此簡單範例中,函式的工作是串連名字和姓氏。 藉由使用假數據,您就知道預期的結果,而且可以驗證函式是否正確執行工作。
假貨
Fakes 會取代您通常不會在生產環境中使用的功能,例如使用記憶體內部資料庫,而不是雲端資料庫。
// 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]);
});
});
// 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);
}
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(),
};
});
afterEach(() => {
// Clear all mocks
jest.resetAllMocks();
});
test('should save and return the correct value', () => {
// Spy on the save method
jest.spyOn(fakeDb, 'save');
// Call the function under 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);
});
});
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
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;
let saveSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
fakeDb = new FakeDatabase();
testKey = 'testKey';
testValue = {
first: 'John',
last: 'Jones',
lastUpdated: new Date().toISOString(),
};
// Create a spy on the save method.
saveSpy = vi.spyOn(fakeDb, 'save');
});
afterEach(() => {
// Restore the spied methods.
vi.restoreAllMocks();
});
it('should save and return the correct value', () => {
// Call the function under 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 using vi.spyOn.
expect(saveSpy).toHaveBeenCalledTimes(1);
expect(saveSpy).toHaveBeenCalledWith(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;
}
}
}
// 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;
}
}
}
// 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 類型有助於定義函式所使用的數據類型。 雖然您不需要使用 TypeScript 來運行 Jest 或其他 JavaScript 測試框架,但 TypeScript 是撰寫類型安全 JavaScript 的必要工具。
此應用程式中的函式如下:
| 功能 |
描述 |
|
insertDocument |
將檔案插入資料庫中。
這就是我們想要測試的內容。 |
|
inputVerified |
根據架構驗證輸入數據。 確保數據的格式正確(例如,有效的電子郵件位址、格式正確的URL)。 |
|
cosmos.items.create |
使用 @azure/cosmos 的 Azure Cosmos DB SDK 函式。
這就是我們想要嘲笑的。 它已經有由套件擁有者維護的測試。 我們需要確認 Cosmos DB 函式呼叫是否已進行,並在傳入數據通過驗證時傳回數據。 |
安裝測試框架依賴項
此架構會作為 Node.js LTS 的一部分提供。
在應用程式目錄的根目錄中,使用下列命令安裝 Jest:
npm install -D jest
在應用程式目錄的根目錄中,使用下列命令安裝 Vitest:
npm install -D vitest
更新應用程式中的 package.json,使用新的腳本來測試我們的原始碼檔案。 原始碼檔案是透過比對部分檔名和擴展名來定義。 測試執行器會依照測試檔案的一般命名慣例來尋找檔案: <file-name>.spec.[jt]s。 此模式表示名為 類似下列範例的檔案會解譯為測試檔案,並由測試執行器執行:
-
*
.test.js:例如,math.test.js
-
*
.spec.js:例如,math.spec.js
-
位於測試目錄中的檔案,例如test/math.js
將文稿新增至 package.json ,以支援測試執行器的測試檔案模式:
"scripts": {
"test": "node --test --experimental-test-coverage --experimental-test-module-mocks --trace-exit"
}
更新應用程式中的 package.json,使用新的腳本來測試我們的原始碼檔案。 原始碼檔案是透過比對部分檔名和擴展名來定義。 Jest 會依照測試檔案的常見命名慣例來尋找檔案: <file-name>.spec.[jt]s。 此模式表示名為 類似下列範例的檔案會解譯為測試檔案,並由 Jest 執行:
-
*
.test.js:例如,math.test.js
-
*
.spec.js:例如,math.spec.js
-
位於測試目錄中的檔案,例如test/math.js
將腳本新增至 package.json ,以支援 Jest 的測試檔案模式:
"scripts": {
"test": "jest dist --coverage",
}
TypeScript 原始程式碼會生成到 dist 子資料夾中,在那裡 Jest 會找到並執行 *.spec.js 檔案。
更新應用程式中的 package.json,使用新的腳本來測試我們的原始碼檔案。 原始碼檔案是透過比對部分檔名和擴展名來定義。 Vitest 會依照測試檔案的一般命名慣例來尋找檔案: <file-name>.spec.[jt]s。 此模式表示名為 類似下列範例的檔案會解譯為測試檔案,並由 Vitest 執行:
-
*
.test.js:例如,math.test.js
-
*
.spec.js:例如,math.spec.js
-
位於測試目錄中的檔案,例如test/math.js
將腳本新增至 package.json ,以支援 Vitest 的測試檔案模式:
"scripts": {
"test": "vitest run --coverage",
}
設定 Azure SDK 的單元測試
如何使用模擬、存根和假物件來測試 insertDocument 函式?
- 模擬:我們需要模擬,以確保 函式的行為 已經過測試,例如:
- 如果數據確實通過驗證,則對 Cosmos DB 函式的呼叫只會發生 1 次
- 如果數據未通過驗證,則不會呼叫 Cosmos DB 函式
- 存根:
測試時,請考慮測試設定、測試本身和驗證。 就測試白話而言,此功能會使用下列詞彙:
- 安排:設定測試條件
- Act:呼叫您的函式以進行測試,也稱為 測試中的系統 或 SUT
- 斷言:驗證結果。 結果可以是行為或狀態。
- 行為表示測試函式中可驗證的功能。 其中一個範例是呼叫一些相依性。
- 狀態表示從函式傳回的數據。
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
});
});
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
});
});
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
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 。
測試檔案有數個部分:
-
import:import 語句可讓您使用或模擬任何測試。
-
jest.mock:建立您想要的預設模擬行為。 每個測試都可以視需要改變。
-
describe:測試檔案的 insert.ts 群組系列。
-
test:每個測試對應於檔案 insert.ts 。
測試檔案有數個部分:
-
import:import 語句可讓您使用或模擬任何測試。
-
vi.spyOn:建立預設間諜,然後使用.mockReturnValue加入模擬行為。 每個測試都可以視需要改變。
-
describe:測試檔案的 insert.ts 群組系列。
-
it:每個測試對應於檔案 insert.ts 。
測試檔案涵蓋檔案的 insert.ts 三個測試,可分成兩種驗證類型:
| 驗證類型 |
測試 |
快樂路徑: 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,
});
});
});
// insertDocument.test.ts
import { Container } from '../data/connect-to-cosmos';
import { createTestInputAndResult } from '../data/fake-data';
import type { DbDocument, DbError, RawInput } from '../data/model';
import { isDbError, isVerificationErrors } 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('SDK', () => {
let mockContainer: jest.Mocked<Container>;
beforeEach(() => {
// Clear all mocks before each test
jest.resetAllMocks();
// 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,
});
});
});
// insertDocument.test.ts
import { describe, it, beforeEach, expect, vi } from 'vitest';
import type { Container, ItemResponse } from '@azure/cosmos';
import { insertDocument } from '../src/lib/insert.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';
describe('insertDocument', () => {
let fakeContainer: Container;
beforeEach(() => {
// Clear all mocks before each test
vi.restoreAllMocks();
// Create a fake container with a mocked `create` function
fakeContainer = {
items: {
create: vi.fn(),
},
} as unknown as Container;
});
it('should return verification error if input is not verified', async () => {
// Arrange – mock the input verification function to return false.
const inputVerifiedMock = vi.spyOn(Verify, 'inputVerified');
inputVerifiedMock.mockReturnValue(false);
const doc = { name: 'test' };
// Act – call the function under test.
const insertDocumentResult = await insertDocument(
fakeContainer,
doc as unknown as RawInput,
);
// Assert – state verification: result should indicate verification failure.
if (isVerificationErrors(insertDocumentResult)) {
expect(insertDocumentResult).toEqual({
message: 'Verification failed',
} as unknown as DbError);
} else {
throw new Error('Result is not of type VerificationErrors');
}
// Assert – behavior verification: ensure create method was not called.
expect(fakeContainer.items.create).not.toHaveBeenCalled();
expect(inputVerifiedMock).toHaveBeenCalledTimes(1);
});
it('should insert document successfully', async () => {
// Prepare test data
const { input, result }: { input: RawInput; result: Partial<DbDocument> } =
createTestInputAndResult();
const inputVerifiedMock = vi.spyOn(Verify, 'inputVerified');
inputVerifiedMock.mockReturnValue(true);
// Set up the mocked return value.
// Here we "cast" our minimal object to satisfy the expected type ItemResponse<DbDocument>.
(
fakeContainer.items.create as unknown as ReturnType<typeof vi.fn>
).mockResolvedValue({
resource: result,
// Minimal additional properties required by ItemResponse.
item: result,
headers: {},
statusCode: 201,
diagnostics: {} as any,
requestCharge: 0,
activityId: 'fake-activity-id',
} as unknown as ItemResponse<DbDocument>);
// Call the function under test that internally calls container.items.create.
const insertDocumentResult = await insertDocument(fakeContainer, input);
// Validate the returned value.
expect(insertDocumentResult).toEqual(result);
// Validate that create was called once with the proper arguments.
expect(inputVerifiedMock).toHaveBeenCalledTimes(1);
expect(fakeContainer.items.create).toHaveBeenCalledTimes(1);
expect(
(fakeContainer.items.create as unknown as ReturnType<typeof vi.fn>).mock
.calls[0][0],
).toEqual({
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 to return true.
const inputVerifiedMock = vi.spyOn(Verify, 'inputVerified');
inputVerifiedMock.mockReturnValue(true);
// Arrange – mock the create method to reject with an error.
const mockError: DbError = {
message: 'An unknown error occurred',
code: 500,
};
(
fakeContainer.items.create as unknown as ReturnType<typeof vi.fn>
).mockRejectedValue(mockError as unknown as DbError);
// Act – call the function under test.
const insertDocumentResult = await insertDocument(fakeContainer, input);
// Assert – verify result is of type 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 was called once with correct arguments.
expect(inputVerifiedMock).toHaveBeenCalledTimes(1);
expect(fakeContainer.items.create).toHaveBeenCalledTimes(1);
expect(
(fakeContainer.items.create as unknown as ReturnType<typeof vi.fn>).mock
.calls[0][0],
).toEqual({
id: input.id,
name: result.name,
});
});
});
故障排除
本文中的大部分程式代碼都來自 MicrosoftDocs/node-essentials GitHub 存放庫。 如果您想要插入 Cosmos DB 雲端資源, 請使用此腳本建立資源。