Partilhar 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 deseja simular chamadas SDK de serviço de nuvem ou usar um serviço ativo 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 recurso para que o recurso de nuvem não seja usado.

As estruturas utilizadas são:

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

Pré-requisitos

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

O Node.js test runner faz parte da instalação Node.js.

Atenção

A amostra fornecida para o executor de teste Node.js usa o módulo experimental node:test com o mock.fn(). Lembre-se de que o executor de testes integrado do Node ainda não oferece uma API de simulação totalmente suportada. Certifique-se de que a sua versão de destino do Node.js ofereça suporte às APIs experimentais ou considere usar uma biblioteca de simulação de terceiros (ou funções falsas).

Serviços na nuvem simulados

Prós:

  • Acelera o conjunto de testes, eliminando 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 ao vivo, especialmente em pipelines de integração contínua.

Contras:

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

Usando um serviço ao vivo

Prós:

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

Contras:

  • É mais lento devido a chamadas de rede.
  • É mais caro devido aos potenciais custos de utilização do serviço.
  • É complexo e demorado configurar e manter um ambiente de serviço ao vivo que corresponda à produção.

A escolha entre simular e utilizar serviços em tempo real depende da sua estratégia de teste. Para testes de unidade onde a velocidade e o controle são primordiais, simular é muitas vezes a melhor escolha. Para testes de integração onde o realismo é crucial, usar um serviço ao vivo pode fornecer resultados mais precisos. O equilíbrio dessas abordagens ajuda a obter uma cobertura abrangente de testes e, ao mesmo tempo, gerencia custos e mantém a eficiência dos testes.

Duplas de teste: Simulações, stubs e falsificações

Um simulacro de teste é qualquer tipo de substituto usado no lugar de algo real para fins de teste. O tipo de duplo que você escolhe é baseado no que você deseja que ele substitua. O termo mock é muitas vezes entendido como qualquer duplo quando o termo é usado casualmente. Neste artigo, o termo é usado especificamente e ilustrado especificamente na estrutura de teste Jest.

Simulações

Mocks (também chamados de espiões): Substituem uma função e podem controlar e espionar o comportamento dessa função quando ela é chamada indiretamente por outro código.

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

  • someTestFunction: A função que você precisa testar. Ele chama uma dependência, dependencyFunction que tu não escreveste e não precisas testar.
  • dependencyFunctionMock: Simulação da 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);

O objetivo 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 decidir simular uma dependência, pode optar por simular exatamente o que necessita, como:

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

Esboços

O objetivo de um stub é substituir os dados de retorno de uma função para simular diferentes cenários. 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 garante que o 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 nome de família. Ao usar dados falsos, você sabe o resultado esperado e pode validar que 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 usar 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]);
  });
});

O objetivo do teste anterior é garantir que someTestFunction interage corretamente com o banco de dados. Usando 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ê tem um aplicativo que precisa escrever um novo documento para o 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 deve 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 o Jest ou outras estruturas de teste de JavaScript, ele é essencial para escrever JavaScript seguro para digitação.

As funções nesta aplicação são:

Função Descrição
inserirDocumento Insere um documento na base de dados. É isso que queremos testar.
inputVerificado Verifica os dados de entrada em relação a um esquema. Garante que os dados estejam no formato correto (por exemplo, endereços de e-mail válidos, URLs formatados corretamente).
cosmos.items.create Função SDK para o Azure Cosmos DB usando o @azure/cosmos. É disso que queremos zombar. Ele já tem seus próprios testes mantidos pelos proprietários do pacote. Precisamos verificar se a chamada de função do Cosmos DB foi feita e se os dados foram retornados, caso os dados recebidos passem na verificação.

Instalar dependência do framework de teste

Esta 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 ficheiros de código-fonte são definidos pela correspondência parcial do nome do ficheiro e 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 os arquivos nomeados como os exemplos a seguir 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 ?

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

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

  • Organizar: configure suas condições de teste
  • Agir: chame sua função para testar, também conhecida como o sistema em teste ou SUT
  • Asserir: validar 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 invocada.
    • State 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 simulações para testar a função sem chamar a dependência subjacente usada na função, como as bibliotecas de cliente do Azure.

Criar o arquivo de teste

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

Há várias partes no ficheiro de teste.

  • import: As instruções de importação permitem que você use ou simule qualquer um dos seus testes.
  • mock: Crie o comportamento simulado padrão desejado. Cada teste pode ser alterado conforme necessário.
  • describe: Família de grupo de teste para o insert.ts arquivo.
  • it: Cada teste para o ficheiro insert.ts.

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

Tipo de validação Teste
Caminho feliz: should insert document successfully O método de banco de dados simulado foi chamado e retornou os dados alterados.
Caminho do erro: should return verification error if input is not verified Os dados falharam na validação e retornaram um erro.
Caminho do erro: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,
    });
  });
});

Solução de problemas

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

Informações adicionais