Поделиться через


Тестирование интеграции пакета SDK Azure в приложениях JavaScript

Тестирование кода интеграции для Пакета SDK Azure для JavaScript важно для обеспечения правильного взаимодействия приложений со службами Azure. В этом руководстве показано, как эффективно протестировать интеграцию пакета SDK Azure в приложениях JavaScript с платформой тестирования.

При выборе между моделированием вызовов SDK облачных служб или использованием реальной службы для тестирования, важно учитывать компромиссы между скоростью, надежностью и стоимостью. В этой статье показано, как использовать тестовую платформу для тестирования интеграции пакета SDK. Код приложения вставляет документ в Cosmos DB. Тестовый код имитирует использование ресурсов, чтобы облачный ресурс не использовался.

Используемые платформы:

  • Jest с CommonJs
  • Vitest с ESM
  • Node.js тестовый раннер с использованием ESM

Предпосылки

Node.js LTS. Состояние выпуска LTS — это "долгосрочная поддержка", которая обычно гарантирует исправление критических ошибок в течение 30 месяцев.

Средство выполнения тестированияNode.js является частью установки Node.js.

Осторожность

В примере, предоставленном для тестового модуля Node.js, используется экспериментальный модуль node:test с mock.fn(). Помните, что встроенный тестировщик Node пока не предлагает полностью поддерживаемый API для макетирования. Убедитесь, что ваша версия Node.js поддерживает экспериментальные API, или рассмотрите возможность использования сторонней библиотеки моделирования (или функции-заглушки) вместо этого.

Макет облачных служб

Преимущества.

  • Ускоряет набор тестов, устраняя задержку сети.
  • Предоставляет прогнозируемые и управляемые тестовые среды.
  • Проще имитировать различные сценарии и пограничные варианты.
  • Сокращает затраты, связанные с использованием динамических облачных служб, особенно в конвейерах непрерывной интеграции.

Недостатки.

  • Макеты могут отходить от фактического пакета SDK, что приводит к несоответствиям.
  • Может игнорировать некоторые функции или поведение живого сервиса.
  • Менее реалистичная среда по сравнению с рабочей средой.

Использование живой службы

Преимущества.

  • Является ли реалистичная среда, которая тесно отражает производство?
  • Полезно ли выполнять тесты интеграции, чтобы обеспечить совместную работу разных частей системы?
  • Полезно ли определить проблемы, связанные с надежностью сети, доступностью служб и фактической обработкой данных?

Недостатки.

  • Медленнее из-за сетевых вызовов.
  • Дороже из-за возможных расходов на использование услуг.
  • Сложно и трудоемко настроить и поддерживать живую сервисную среду, соответствующую рабочей.

Выбор между моделированием и использованием реальных служб зависит от стратегии тестирования. Для модульных тестов, где важны скорость и контроль, мокинг часто является лучшим выбором. Для тестов интеграции, где реалистичность имеет решающее значение, использование динамической службы может обеспечить более точные результаты. Балансировка этих подходов помогает обеспечить комплексное покрытие тестов при управлении затратами и поддержании эффективности тестирования.

Тестовые двойники: моки, заглушки и фейки

Тестовый двойник — это любая замена, используемая вместо чего-то реального для тестирования. Тип выбранного двойника основан на том, что вы хотите заменить. Термин имитация часто означает любое удвоение, когда он используется в неформальной речи. В этой статье термин используется специально и иллюстрируется специально в тестовой платформе Jest.

Макеты

Моки (также называемые шпионами): можно заменить функцию и иметь возможность контролировать и шпионить за поведением этой функции, когда она вызывается косвенно другим кодом.

В следующих примерах у вас есть 2 функции:

  • someTestFunction: функция, которую нужно протестировать. Он вызывает зависимость, dependencyFunction, которую вы не писали и вам не нужно тестировать.
  • зависимостиFunctionMock: макет зависимости.
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);

Целью теста является обеспечение правильности поведения некоторыхTestFunction без фактического вызова кода зависимостей. Тест проверяет, был ли вызван макет зависимости.

Макет больших и небольших зависимостей

Когда вы решите издеваться над зависимостью, вы можете выбрать только то, что вам нужно, например:

  • Функция или две из более крупной зависимости. 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 правильного взаимодействия с базой данных. Используя фиктивную базу данных в памяти, вы можете протестировать логику функции без использования реальной базы данных, что делает тесты более быстрыми и надежными.

Сценарий. Вставка документа в Cosmos DB с помощью пакета SDK Azure

Представьте, что у вас есть приложение, которое должно написать новый документ в Cosmos DB, если все сведения отправлены и проверены. Если пустая форма отправлена или информация не соответствует ожидаемому формату, приложение не должно вводить данные.

Cosmos DB используется в качестве примера, однако основные понятия применяются к большинству пакетов SDK Azure для JavaScript. Следующая функция фиксирует эту функцию:

// 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 Функция SDK для Azure Cosmos DB с помощью @azure/cosmos. Это то, что мы хотим высмеять. У него уже есть собственные тесты, поддерживаемые владельцами пакетов. Необходимо убедиться, что вызов функции Cosmos DB был выполнен, и данные были возвращены, если входящие данные прошли проверку.

Установка зависимостей платформы тестирования

Эта платформа предоставляется как часть Node.js LTS.

Настройка пакета для выполнения теста

package.json Обновите приложение с помощью нового скрипта, чтобы протестировать файлы исходного кода. Файлы исходного кода определяются путем сопоставления с частичным именем и расширением файла. Среда выполнения тестов ищет файлы, следуя стандартам именования для тестовых файлов: <file-name>.spec.[jt]s. Этот шаблон означает, что файлы, такие как следующие примеры, интерпретируются как тестовые файлы и запускаются средством запуска тестов:

  • * .test.js: например, math.test.js
  • * .spec.js: например, math.spec.js
  • Файлы, расположенные в каталоге тестов, например тесты илиmath.js

Добавьте скрипт в package.json для поддержки этого шаблона тестового файла с помощью средства выполнения тестов:

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

Настройка модульного теста для Azure SDK

Как можно использовать mock-объекты, stub-объекты и fake-объекты для тестирования функции 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: Операторы импорта позволяют использовать или симулировать любой тест.
  • mock: создайте нужное поведение имитации по умолчанию. Каждый тест может изменяться по мере необходимости.
  • 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,
    });
  });
});

Устранение неполадок

Большая часть кода в этой статье поступает из репозитория GitHub MicrosoftDocs/node-essentials . Если вы хотите вставить в облачный ресурс Cosmos DB, создайте ресурс с помощью этого скрипта.

Дополнительные сведения