测试用于 JavaScript 的 Azure SDK 的集成代码对于确保应用程序与 Azure 服务正确交互至关重要。 本指南介绍如何在 JavaScript 应用程序中有效地测试测试测试框架中的 Azure SDK 集成。
在决定是否模拟云服务 SDK 调用或使用实时服务进行测试时,务必考虑速度、可靠性和成本之间的权衡。 本文演示如何使用测试框架测试 SDK 集成。 应用程序代码将文档插入 Cosmos DB。 测试代码模拟资源使用情况,以便不使用云资源。
使用的框架包括:
- CommonJs 的 Jest
- ESM 的 Vitest
- ESM 的 Node.js 测试运行程序
先决条件
Node.js LTS。 LTS 发布状态为“长期支持”,这通常可保证严重的 bug 将总共修复 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);
// 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 为此目的提供了部分模拟。
-
较小依赖项的所有函数,如本文中的示例所示。
存根
存根的目的是替换函数的返回数据来模拟不同的方案。 使用存根允许代码调用函数并接收各种状态,包括成功结果、失败、异常和边缘情况。
状态验证可确保代码正确处理这些方案。
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
完成的工作符合预期结果。 在此简单示例中,函数的任务是连接名字和姓氏。 通过使用虚设数据,你知道预期结果,并且可以验证函数是否正确执行工作。
假货
虚设会取代通常不会在生产中使用的功能,例如使用内存中数据库,而不是云数据库。
// 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 测试框架,但它对于编写类型安全的 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
-
位于 tests 目录中的文件,例如 tests/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
-
位于 tests 目录中的文件,例如 tests/math.js
将脚本添加到 package.json,以支持使用 Jest 的该测试文件模式:
"scripts": {
"test": "jest dist --coverage",
}
TypeScript 源代码将生成到 Jest 将在其中查找并运行 dist
文件的 *.spec.js
子文件夹中。
使用新脚本更新应用程序的 package.json
,以测试源代码文件。 通过匹配部分文件名和扩展名,可定义源代码文件。 Vitest 根据测试文件的常见命名约定查找文件:<file-name>.spec.[jt]s
此模式意味着名为以下示例的文件被解释为测试文件,并由 Vitest 运行:
-
*
.test.js:例如,math.test.js
-
*
.spec.js:例如,math.spec.js
-
位于 tests 目录中的文件,例如 tests/math.js
将脚本添加到 package.json 以支持 Vitest 的测试文件模式:
"scripts": {
"test": "vitest run --coverage",
}
为 Azure SDK 设置单元测试
如何使用模拟对象、存根和伪造对象来测试insertDocument函数?
- 模拟:我们需要模拟才能确保函数的行为经过测试,例如:
- 如果数据确实通过验证,则对 Cosmos DB 函数的调用仅执行 1 次
- 如果数据未通过验证,则不会调用 Cosmos DB 函数
- 存根:
测试时,考虑测试设置、测试本身和验证。 在测试白话方面,此功能使用以下术语:
- 排列:设置测试条件
- 操作:调用函数进行测试,也称为受测系统或 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 云资源, 请使用此脚本创建资源。