在 JavaScript 应用程序中测试 Azure SDK 集成
测试用于 JavaScript 的 Azure SDK 的集成代码对于确保应用程序与 Azure 服务正确交互至关重要。
在决定是否模拟云服务 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 的行为正确,而无需实际调用依赖项代码。 测试会验证是否调用了依赖项的模拟。
模拟大型依赖项与小型依赖项
决定模拟依赖项时,可以选择模拟所需的内容,例如:
- 一个或两个来自较大依赖项的函数。 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;
}
}
}
注意
TypeScript 类型有助于定义函数使用的数据类型。 虽然不需要 TypeScript 使用 Jest 或其他 JavaScript 测试框架,但它对于写入类型安全的 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 目录中的文件,例如 tests/math.js
将脚本添加到 package.json,以支持使用 Jest 的该测试文件模式:
"scripts": {
"test": "jest dist",
}
TypeScript 源代码将在 dist
子文件夹中生成。 Jest 会运行在 dist
子文件夹中找到的 .spec.js
文件。
为 Azure SDK 设置单元测试
如何使用模拟、存根和虚设来测试 insertDocument 函数?
- 模拟:我们需要模拟才能确保函数的行为经过测试,例如:
- 如果数据确实通过验证,则对 Cosmos DB 函数的调用仅执行 1 次
- 如果数据未通过验证,则不会调用 Cosmos DB 函数
- 存根:
- 传入的数据与函数返回的新文档匹配。
测试时,考虑测试设置、测试本身和验证。 以测试行话表述时,这称为:
- 排列:设置测试条件
- 操作:调用函数进行测试,也称为受测系统或 SUT
- 断言:验证结果。 结果可以是行为或状态。
- 行为指示测试函数中可以验证的功能。 一个示例是调用了某个依赖项。
- 状态指示从函数返回的数据。
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
文件的三个测试,可分为两种验证类型:
验证类型 | 测试 |
---|---|
正常路径: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