Compartilhar via


Como testar a integração do SDK do Azure em aplicativos JavaScript

Testar seu código de integração para o SDK do Azure para JavaScript é essencial para garantir que seus aplicativos interajam corretamente com os serviços do Azure. Este guia mostra como testar efetivamente a integração do SDK do Azure em seus aplicativos JavaScript uma estrutura de teste.

Ao decidir se deve simular as chamadas do SDK do serviço de nuvem ou usar um serviço ao vivo para fins de teste, é importante considerar as compensações entre velocidade, confiabilidade e custo. Este artigo demonstra como usar uma estrutura de teste para testar a integração do SDK. O código do aplicativo insere um documento no Cosmos DB. O código de teste simula esse uso de recursos para que o recurso de nuvem não seja usado.

As estruturas usadas são:

  • Jest com CommonJs
  • Vitest com ESM
  • Executor de testes Node.js com ESM

Pré-requisitos

Node.js LTS. O status de lançamento LTS é "suporte de longo prazo", o que normalmente garante que bugs críticos serão corrigidos por um período total de 30 meses.

O Node.js executor de teste faz parte da instalação do Node.js.

Cuidado

O exemplo fornecido para o executor de teste Node.js utiliza o módulo experimental node:test com mock.fn(). Tenha em mente que o executor de teste interno do Node ainda não oferece uma API de zombaria totalmente suportada. Certifique-se de que a versão do Node de destino seja compatível com as APIs experimentais ou considere usar uma biblioteca de simulação de terceiros (ou funções stub).

Simulação de serviços de nuvem

Vantagens:

  • Acelera o conjunto de testes ao eliminar a latência da rede.
  • Fornece ambientes de teste previsíveis e controlados.
  • Mais fácil de simular vários cenários e casos extremos.
  • Reduz os custos associados ao uso de serviços de nuvem ativos, especialmente em pipelines de integração contínua.

Desvantagens:

  • As simulações podem se desviar do SDK real, o que leva a discrepâncias.
  • Pode ignorar determinados recursos ou comportamentos do serviço ao vivo.
  • Ambiente menos realista em comparação com a produção.

Usar um serviço ao vivo

Vantagens:

  • É um ambiente realista que reflete de perto a produção?
  • É útil para testes de integração para garantir que diferentes partes do sistema funcionem juntas?
  • É útil identificar problemas relacionados à confiabilidade da rede, à disponibilidade do serviço e ao tratamento real de dados?

Desvantagens:

  • É mais lento devido a chamadas de rede.
  • É mais caro devido a custos potenciais de uso do serviço.
  • É complexo e demorado configurar e manter um ambiente de serviço dinâmico que corresponda à produção.

A escolha entre a simulação e o uso de serviços ao vivo depende de sua estratégia de teste. Para testes de unidade em que a velocidade e o controle são primordiais, a simulação geralmente é a melhor escolha. Para testes de integração em que o realismo é crucial, o uso de um serviço ao vivo pode fornecer resultados mais precisos. Equilibrar essas abordagens ajuda a obter uma cobertura de teste abrangente, gerenciando custos e mantendo a eficiência do teste.

Duplicatas de teste: simulações, stubs e falsificações

Uma duplicata de teste é qualquer tipo de substituto usado no lugar de algo real para fins de teste. O tipo de duplicata que você escolhe é baseado no que você deseja que ela substitua. O termo mock é frequentemente entendido como qualquer duplicata quando o termo é usado casualmente. Neste artigo, o termo é usado e ilustrado especificamente na estrutura de teste do Jest.

Simulações

Simulações (também chamadas de espiões): substituem uma função e são capazes de controlar e espionar o comportamento dessa função quando ela é chamada indiretamente por algum outro código.

Nos exemplos a seguir, você tem 2 funções:

  • someTestFunction: a função que você precisa testar. Ele invoca uma dependência, dependencyFunction, que você não escreveu e não precisa testar.
  • dependencyFunctionMock: simular a dependência.
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);

A finalidade do teste é garantir que someTestFunction se comporte corretamente sem realmente invocar o código de dependência. O teste valida que a simulação da dependência foi chamada.

Simular dependências grandes versus pequenas

Quando você decide simular uma dependência, pode optar por simular apenas o que precisa, por exemplo:

  • Uma ou duas funções de uma dependência maior. Jest oferece simulações parciais para esse fim.
  • Todas as funções de uma dependência menor, conforme mostrado no exemplo deste artigo.

Stubs

A finalidade de um stub é substituir os dados de retorno de uma função para simular cenários diferentes. Você usa um stub para permitir que seu código chame a função e receba vários estados, incluindo resultados bem-sucedidos, falhas, exceções e casos de borda. A verificação de estado assegura que seu código trate esses cenários corretamente.

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

O objetivo do teste anterior é garantir que o trabalho realizado por someTestFunction atenda ao resultado esperado. Neste exemplo simples, a tarefa da função é concatenar o primeiro nome e o sobrenome. Ao usar dados falsos, você sabe o resultado esperado e pode validar se a função executa o trabalho corretamente.

Falsificações

As falsificações substituem uma funcionalidade que você normalmente não usaria na produção, como o uso de um banco de dados na memória em vez de um banco de dados na nuvem.

// 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]);
  });
});

A finalidade do teste anterior é garantir que someTestFunction interaja corretamente com o banco de dados. Ao usar um banco de dados falso na memória, você pode testar a lógica da função sem depender de um banco de dados real, tornando os testes mais rápidos e confiáveis.

Cenário: inserir documento no Cosmos DB usando o SDK do Azure

Imagine que você tenha uma aplicação que precisa gravar um novo documento no Cosmos DB, se todas as informações forem enviadas e verificadas. Se um formulário vazio for enviado ou as informações não corresponderem ao formato esperado, o aplicativo não deverá inserir os dados.

O Cosmos DB é usado como exemplo, no entanto, os conceitos se aplicam à maioria dos SDKs do Azure para JavaScript. A função a seguir captura essa funcionalidade:

// 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;
    }
  }
}

Observação

Os tipos TypeScript ajudam a definir os tipos de dados que uma função usa. Embora você não precise do TypeScript para usar Jest ou outras estruturas de teste JavaScript, ele é essencial para escrever JavaScript com tipagem segura.

As funções neste aplicativo são:

Função Descrição
insertDocument Insere um documento no banco de dados. Isso é o que queremos testar.
inputVerified Verifica os dados de entrada em relação a um esquema. Garante que os dados estejam no formato correto (por exemplo, endereços de email válidos, URLs formatados corretamente).
cosmos.items.create Função de SDK para o Azure Cosmos DB usando @azure/cosmos. Isso é o que queremos simular. Ele já tem seus próprios testes mantidos pelos proprietários dos pacotes. Precisamos verificar se a chamada da função do Cosmos DB foi feita e se os dados retornados foram aprovados na verificação.

Instalar dependência da estrutura de teste

Essa estrutura é fornecida como parte do Node.js LTS.

Configurar o pacote para executar o teste

Atualize o package.json para o aplicativo com um novo script para testar nossos arquivos de código-fonte. Os arquivos de código-fonte são definidos pela correspondência entre o nome parcial do arquivo e a extensão. O executor de teste procura arquivos seguindo a convenção de nomenclatura comum para arquivos de teste: <file-name>.spec.[jt]s. Esse padrão significa que arquivos nomeados como os seguintes exemplos são interpretados como arquivos de teste e executados pelo executor de teste:

  • * .test.js: por exemplo, math.test.js
  • * .spec.js: por exemplo, math.spec.js
  • Arquivos localizados em um diretório de testes, como tests/math.js

Adicione um script ao package.json para dar suporte a esse padrão de arquivo de teste com o executor de teste:

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

Configurar o teste de unidade para o SDK do Azure

Como podemos usar simulações, stubs e falsificações para testar a função insertDocument?

  • Simulações: precisamos de um mock para garantir que o comportamento da função seja testado como:
    • Se os dados passarem na verificação, a chamada para a função do Cosmos DB ocorreu apenas 1 vez
    • Se os dados não passarem na verificação, a chamada para a função do Cosmos DB não aconteceu
  • Stubs:
    • Os dados passados correspondem ao novo documento retornado pela função.

Ao testar, pense em termos de configuração do teste, do próprio teste e da verificação. Em termos de vernáculo de teste, essa funcionalidade usa os seguintes termos:

  • Organizar: configurar suas condições de teste
  • Ato: chame sua função para testar, também conhecida como sistema em teste ou SUT
  • Afirmar: valida os resultados. Os resultados podem ser comportamento ou estado.
    • O comportamento indica a funcionalidade em sua função de teste, que pode ser verificada. Um exemplo é que alguma dependência foi chamada.
    • Estado indica os dados retornados da função.
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
  });
});

Quando você usa simulações em seus testes, esse código de modelo precisa usar a simulação para testar a função sem chamar a dependência subjacente usada na função, como as bibliotecas de clientes do Azure.

Criar o arquivo de teste

O arquivo de teste com simulações, para simular uma chamada a uma dependência, tem uma configuração adicional.

Há várias partes no arquivo de teste:

  • import: As instruções de importação permitem que você use ou simule qualquer um dos seus testes.
  • mock: crie o comportamento de simulação padrão que você deseja. Cada teste pode ser alterado conforme necessário.
  • describe: família de grupos de teste para o arquivo insert.ts.
  • it: cada teste para o arquivo insert.ts.

O arquivo de teste abrange três testes para o arquivo insert.ts, que podem ser divididos em dois tipos de validação:

Tipo de validação Teste
Bom caminho: should insert document successfully O método de banco de dados simulado foi chamado e retornou os dados alterados.
Erro de caminho: should return verification error if input is not verified Os dados falharam na validação e retornaram um erro.
Erro de caminho:should return error if db insert fails O método de banco de dados simulado foi chamado e retornou um erro.

O arquivo de teste a seguir mostra como testar a função 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,
    });
  });
});

Resolução de problemas

A maior parte do código neste artigo vem do repositório GitHub do MicrosoftDocs/node-essentials . Se você quiser inserir em um recurso de nuvem do Cosmos DB, crie o recurso com esse script.

Informações adicionais