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


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

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

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

Необходимые компоненты

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

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

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

Недостатки.

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

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

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

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

Недостатки.

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

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

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

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

Макеты

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

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

  • someTestFunction: функция, которую нужно протестировать. Он вызывает зависимость, dependencyFunctionкоторая не была написана и не требуется тестировать.
  • зависимостиFunctionMock: макет зависимости.
// setup
const dependencyFunctionMock = jest.fn();

// perform test
// Jest replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()

// verify behavior
expect(dependencyFunctionMock).toHaveBeenCalled();

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

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

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

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

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

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

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

// 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.

Функции в этом приложении:

Function Description
insertDocument Вставляет документ в базу данных. Это то, что мы хотим проверить.
inputVerified Проверяет входные данные по схеме. Гарантирует, что данные имеют правильный формат (например, допустимые адреса электронной почты, правильно отформатированные URL-адреса).
cosmos.items.create Функция SDK для Azure Cosmos DB с помощью @azure/cosmos. Это то, что мы хотим насмехать. У него уже есть собственные тесты, поддерживаемые владельцами пакетов. Необходимо убедиться, что вызов функции 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
  • Файлы, расположенные в каталоге тестов, например тесты/math.js

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

"scripts": {
    "test": "jest dist",
}

Исходный код TypeScript создается в вложенную папку dist . Jest запускает файлы, найденные .spec.js в подпапке dist .

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

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

Дополнительная информация: