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.
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.
Pré-requisitos
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, levando 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:
- Fornece 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.
- Ajuda a identificar problemas relacionados à confiabilidade da rede, à disponibilidade do serviço e ao manuseio de dados reais.
Desvantagens:
- Mais lento devido às chamadas de rede.
- Mais caro devido aos potenciais custos de uso do serviço.
- Complexo e demorado para configurar e manter um ambiente de serviço ao vivo 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: esta é a função que você precisa testar. Ele chama uma dependência,
dependencyFunction
, que 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();
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, como:
- 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 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 extremos. 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 nome e os nomes de família. Ao usar dados falsos, você sabe o resultado esperado e pode validar se a função executa o trabalho corretamente.
Fakes
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
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);
});
});
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 um documento no Cosmos DB usando o SDK do Azure
Imagine que você tenha um aplicativo 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';
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;
}
}
}
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 de tipo seguro.
As funções neste aplicativo acima 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 | 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
Este artigo usa Jest como estrutura de teste. Há outras estruturas de teste 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 entre o nome parcial do arquivo e a extensão. O Jest procura por arquivos que sigam 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 suportar esse padrão de arquivo de teste com Jest:
"scripts": {
"test": "jest dist",
}
O código-fonte do TypeScript é gerado na subpasta dist
. Jest executa os .spec.js
arquivos encontrados na subpasta dist
.
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, isso é conhecido como:
- 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.
O Jest, semelhante a outras estruturas de teste, tem um arquivo de teste clichê 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, o local do boiler 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 para uma dependência tem alguma configuração extra além do código clichê de teste comum. Há várias partes no 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 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 arquivoinsert.ts
.test
: cada teste para o arquivoinsert.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 Jest a seguir 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
- Melhores práticas de simulação de Jest
- A diferença entre simulações e stubs por Martin Fowler