如何在 JavaScript 应用程序中测试 Azure SDK 集成

测试用于 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);

测试的目的是确保 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}`);

上述测试的目的是确保 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 类型有助于定义函数使用的数据类型。 虽然不需要 TypeScript 使用 Jest 或其他 JavaScript 测试框架,但它对于编写类型安全的 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 目录中的文件,例如 tests/math.js

将脚本添加到 package.json ,以支持该测试文件模式与测试运行程序:

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

为 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
  });
});

在测试中使用模拟时,该模板代码需要使用模拟来测试函数,而无需调用函数中使用的基础依赖项,例如 Azure 客户端库。

创建测试文件

带有模拟的测试文件,用于模拟对依赖项的调用,具有额外的设置。

测试文件有几个部分:

  • import:利用 import 语句,可使用或模拟任何测试。
  • mock:创建所需的默认模拟行为。 每个测试都可以根据需要进行更改。
  • describeinsert.ts 文件的测试组系列。
  • itinsert.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,
    });
  });
});

故障排除

本文中的大多数代码来自 MicrosoftDocs/node-essentials GitHub 存储库。 如果要插入 Cosmos DB 云资源, 请使用此脚本创建资源

其他信息