Partilhar via


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

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.

Pré-requisitos

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 de borda.
  • 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:

  • Fornece um ambiente realista que espelha de perto a produção.
  • Útil para testes de integração para garantir que diferentes partes do sistema trabalhem juntas.
  • Ajuda a 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 para configurar e manter um ambiente de serviço ao vivo que corresponda à produção.

A escolha entre zombar e usar serviços ao vivo 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 duplo 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 em 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: Esta é a função que você precisa testar. Ele chama de dependência, dependencyFunctionque você não escreveu e não precisa testar.
  • dependencyFunctionMock: Esta é uma simulação da dependência.
// setup
const dependencyFunctionMock = jest.fn();

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

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

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 você decide zombar de uma dependência, você pode optar por zombar exatamente do que você precisa, 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.

Stubs

O objetivo dos stubs é substituir os dados de retorno de uma função para simular diferentes cenários. Isso permite 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 seu código lide com esses cenários corretamente.

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

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

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: Inserindo um 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';
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;
    }
  }
}

Nota

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 acima são:

Function Description
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 os dados retornados se os dados recebidos passarem na verificação.

Instalar dependência da estrutura de teste

Este artigo usa Jest como a estrutura de teste. Existem outras estruturas de teste, que são comparáveis que você também pode usar.

Na raiz do diretório do aplicativo, instale o Jest com o seguinte comando:

npm install jest

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 em nome de arquivo parcial e extensão. Jest 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 serão interpretados como arquivos de teste e executados pelo Jest:

  • *.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 Jest:

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

O código-fonte TypeScript é gerado na dist subpasta. Jest executa os .spec.js arquivos encontrados na dist subpasta.

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, isto é conhecido como:

  • Organizar: configure suas condições de teste
  • Agir: chame sua função para testar, também conhecida como o sistema em teste ou SUT
  • Afirmar: 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 chamada.
    • State indica os dados retornados da função.

Jest, semelhante a outras estruturas de teste, tem clichê de arquivo de teste para definir seu arquivo de teste.

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

Ao usar simulações, esse local de caldeira 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 alguma configuração extra além do código clichê de teste comum. Há várias partes para o arquivo de teste abaixo:

  • import: As instruções de importação permitem que você use ou simule qualquer um dos seus testes.
  • jest.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.
  • test: Cada teste para o insert.ts arquivo.

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

Informações adicionais