O teste de software aumenta a qualidade e a confiabilidade do aplicativo. Ele permite que os desenvolvedores encontrem e consertem bugs de software, mitiguem problemas de segurança e simulem casos de uso de usuários reais.
É uma parte essencial do desenvolvimento de qualquer aplicativo.
Serverless é uma tecnologia incrível, quase mágica. Aplicativos serverless, como quaisquer outros aplicativos, exigem testes.
No entanto, testar aplicativos sem servidor é diferente dos testes tradicionais e apresenta novos desafios.
Na primeira parte , você aprendeu por que os serviços sem servidor apresentam novos desafios de teste e minhas diretrizes práticas para testar serviços sem servidor e funções do AWS Lambda que atenuam esses desafios.
Neste post, você aprenderá a escrever testes para seu serviço Serverless. Vamos nos concentrar em funções Lambda e fornecer dicas e truques e exemplos de código escrevendo testes para um aplicativo Serverless real. Além disso, você aprenderá minha adaptação Serverless à pirâmide de testes clássica e a implementará.
Na parte três , você aprenderá a testar fluxos assíncronos orientados a eventos que podem ou não conter funções Lambda e outros serviços Serverless não baseados em Lambda.
Um projeto de serviço Serverless complementar que utiliza as melhores práticas de teste Serverless pode ser encontrado aqui .
Índice
O serviço sem servidor de pedidos
Usaremos este serviço de exemplo e escreveremos testes para seu manipulador de função Lambda.
Aqui está o diagrama de arquitetura:
O serviço 'order' obtém os pedidos dos clientes para um único tipo de produto e salva os pedidos no banco de dados.
Este serviço de exemplo é simples, um API Gateway que aciona uma função Lambda que grava em uma tabela do DynamoDB. Além disso, a função Lambda usa AppConfig para configuração dinâmica e sinalizadores de recursos.
Eu uso o AWS CDK para implantar o serviço de pedidos.
Criei este serviço de modelo sem servidor que incorporou muitas práticas recomendadas no domínio sem servidor, desde CI/CD e CDK até a escrita de bons manipuladores de funções.
O repositório: https://github.com/ran-isenberg/aws-lambda-handler-cookbook
A pirâmide de testes sem servidor
Suposições
As definições abaixo não são acadêmicas; elas são minha definição. A definição não é tão importante quanto a substância. Contanto que você teste esses aspectos do seu aplicativo Serverless, você pode ter confiança na sua qualidade geral.
Embora não seja obrigatório, é melhor você ler as diretrizes de teste sem servidor que apresentei no post anterior da série.
Uso Python nos exemplos, mas os princípios são relevantes para a maioria das linguagens de programação suportadas pelo Lambda.
Vamos analisar a pirâmide de testes sem servidor e entender os valores e objetivos que cada etapa da pirâmide fornece.
Uma pirâmide?!
A "Pirâmide de Testes" é uma metáfora que nos diz para agrupar os testes de software em baldes de granularidade diferente. Ela também dá uma ideia de quantos testes devemos ter em cada um desses grupos. Embora o conceito da Pirâmide de Testes já exista há algum tempo, as equipes ainda lutam para colocá-lo em prática corretamente" - Martin Fowler
Vamos dar sentido a esse diagrama.
O diagrama define quatro níveis de testes: unidade, infraestrutura, integração e ponta a ponta (E2E). Cada teste tem seu propósito e características.
Seguindo minhas diretrizes de teste do Servleress, todos os testes são acionados no IDE, e os desenvolvedores podem adicionar pontos de interrupção ao seu código.
Cada tipo de teste obtém sua pasta na estrutura do projeto sob a pasta principal '/tests'.
Como observação, eu uso 'pytest' para aplicativos baseados em Python para o mecanismo de teste.
Vamos revisar os diferentes tipos de testes e suas características.
Testes Unitários
Os testes unitários testam a funcionalidade de unidades individuais de código. Esses testes são feitos para serem rápidos , fáceis de depurar no IDE com pontos de interrupção e não exigem implantação na AWS, tornando-os isolados .
Os testes unitários entram em ação no código que você escreve, ou seja, no código da função Lambda.
Eu costumo usá-los em dois casos de uso:
Lógica de validação de esquema - Eu uso Pydantic para validação de entrada e validação de esquema (respostas boto , respostas de API, validação de entrada, etc.) casos de uso. O esquema Pydantic pode conter verificações de restrição de tipo e valor ou até mesmo lógica mais complicada com o código validador personalizado.
Teste pequenas funções ou módulos isolados que tenham entrada e saída definidas. Neste caso, quero testar a lógica específica de uma função/módulo interno e verificar sua lógica e efeitos colaterais. Se a função exigir chamadas de API da AWS ou recursos implantados, faça um stub da chamada ou mova o teste para os testes de integração. Não recomendo chamar o manipulador em si neste ponto (isso será feito como parte dos testes de integração), mas apenas pequenas funções isoladas.
Se você quiser saber mais sobre as práticas recomendadas de validação de entrada para funções do AWS Lambda, leia mais aqui .
Exemplos de testes unitários
Vamos escrever um teste de unidade.
A função Lambda de criação de pedidos do serviço de pedidos espera um documento JSON contendo dois parâmetros: 'customer_name' e 'order_item_count'.
Nosso esquema define 'customer_name' como uma sequência de caracteres entre 1 e 20 caracteres e 'order_item_count' como um número inteiro positivo.
Um exemplo de entrada válida se parece com isto:
Os esquemas Pydantic correspondentes se parecem com isto:
Agora, vamos escrever testes de unidade que verificam tipos de entrada válidos e inválidos:
Os testes verificam tantos tipos de esquemas de erro quanto possível, seja um tipo incorreto ou restrições de valor.
O teste na linha 5 verifica a restrição de que o nome do cliente é uma string não vazia, pois espera que o esquema gere uma exceção.
O teste na linha 21 verifica se um valor de contagem de itens de pedido não inteiro gera uma exceção.
Esses testes podem parecer triviais, mas quanto mais lógica e parâmetros você adiciona aos seus esquemas, maior a possibilidade de um bug de produção.
" A validação de entrada deve ser aplicada tanto no nível sintático quanto no semântico . - OWASP
Os testes completos da unidade de serviço de pedidos podem ser encontrados aqui .
Testes de Infraestrutura
Conforme declarado nas diretrizes do meu post anterior sobre testes do Servleress, o código do aplicativo e a infraestrutura residem no mesmo projeto e são implantados juntos.
Depois que seu código e infraestrutura estiverem implantados, não haverá mais como voltar atrás.
Então, queremos garantir que nossa infraestrutura esteja configurada corretamente, que não haja recursos ausentes (estruturas de IaC também têm bugs) e que não tenhamos problemas de segurança.
Queremos verificar esses aspectos antes da implantação, para não interromper nosso ambiente de produção.
Como estou mais familiarizado com ferramentas que usam o CloudFormation, fornecerei ferramentas de teste baseadas nele. Os testes de infraestrutura passarão pelo modelo do CloudFormation que estamos prestes a implantar e verificarão vários problemas:
Recursos críticos ausentes - um bucket/tabela do DynamoDB/etc. foi removido por engano.
Alteração de ID lógico de recursos com estado - quando um ID lógico de um recurso muda, o recurso anterior é excluído e o recurso é recriado novamente. Para recursos com estado, como DynamoDB, pode ocorrer perda de dados.
Problemas de segurança - verifique se as definições de função são menos privilegiadas e se as configurações de recursos são seguras: criptografia em REST, sem buckets S3 públicos e muito mais.
Para obter exemplos de testes de infraestrutura específicos do AWS CDK, acesse meu blog de práticas recomendadas do AWS CDK e confira as seções "Testes do AWS CDK" e "Os padrões de segurança não são bons o suficiente".
Para AWS SAM, confira o linter .
Para modelos genéricos do CloudFormation, consulte CFN-NAG .
Exemplos de testes de infraestrutura
Vamos definir um teste de infraestrutura de segurança do CDK. O teste abrange nossa definição de serviço serverless, do API Gateway ao Lambda role e function e tabela DynamoDB.
A linha 14 sintetiza o template CloudFormation, e a linha 11 executa um conjunto de testes definidos pela matriz de solução da AWS . Uma exceção é gerada em caso de um problema de segurança.
Você pode adicionar mais padrões de segurança; veja mais informações aqui .
Agora vamos definir um teste de infraestrutura do CDK que verifica se nosso recurso crítico, o API Gateway, está definido e não foi excluído por engano ou bug.
A linha 13 sintetiza o template do CloudFormation, e a linha 16 afirma que há um recurso de gateway de API. Você pode expandir esse teste, verificar IDs lógicos de recursos com estado e garantir que eles não tenham mudado.
Para outras práticas recomendadas do AWS CDK, confira minha outra postagem .
Os testes completos de infraestrutura do serviço de pedidos podem ser encontrados aqui .
Testes de Integração
Os testes de integração são a base dos testes de funções Lambda.
Eles testam seu código e como ele se integra e interage com a infraestrutura que você criou na AWS. Você testa sua função Lambda, um módulo de software completo, seja um micro ou nano serviço, do início ao fim da invocação.
Dessa forma, os testes de integração exigem a implantação de seus recursos na AWS e eles normalmente:
Execute após a fase de implantação no pipeline de CI/CD.
Execute localmente no IDE, permita depuração com pontos de interrupção.
Execute localmente no IDE com a função e permissões de desenvolvedor, não com a função do Lambda.
Chame o manipulador de função com um evento de função Lambda gerado para simular uma invocação de integração Lambda real.
Exige a configuração de variáveis de ambiente locais, ganchos ou simulações necessárias para a função no início do teste (veja conftest para Python).
Pode chamar APIs e recursos de serviços da AWS.
Eles geralmente são mais lentos e menos isolados.
Compõem a maioria dos testes de serviço.
Contém testes para casos extremos com simulações (falhas simuladas ou exceções geradas).
Mencionei no item 4 que você deve gerar o evento de função esperado. Há pelo menos três opções que posso pensar para descobrir o exemplo de esquema de evento:
Gere-o na sua conta da AWS, imprima o evento e copie e cole em uma função de fábrica que o retorna para os testes de integração.
Use este repositório de esquemas. Ele contém um número absurdo de esquemas de eventos de amostra.
Leia a documentação do serviço que invoca sua função Lambda. Embora não seja perfeito, muitos serviços AWS melhoraram a documentação e agora incluem eventos de amostra.
Por onde começar
Geralmente desenvolvo um novo manipulador de função Lambda escrevendo um teste de integração de entrada de fluxo "feliz" que chama minha nova função. O fluxo feliz simula um caso de uso de negócios real e entrada. Dessa forma, posso depurar meu código localmente até que o teste passe e usar recursos reais da AWS, também conhecido como estilo TDD .
Outros testes devem simular (com simulações) os seguintes casos de uso:
Erros de APIs da AWS - verifique se lidamos com os erros corretamente, talvez até tente novamente a ação e não falhe.
Gerou exceções em camadas internas, verificou se elas foram capturadas e se a resposta da função está correta (código de erro interno do servidor para HTTP, etc.).
Entrada inválida - verifique se um código de resposta HTTP Bad Request é retornado (quando a função está atrás de um API Gateway).
Configuração de sinalizadores de recursos - Escrevi um post sobre como lidar com testes com sinalizadores de recursos; leia aqui .
Os efeitos colaterais da função Assert ocorreram corretamente - sua função salvou um item no banco de dados? Ele continha os parâmetros esperados?
Exemplos de testes de integração
Vamos escrever um teste de integração para nossa função Lambda da API 'create order'. O Lambda pega um evento de entrada, analisa-o e salva-o em uma tabela do DynamoDB.
Vamos dar uma olhada no manipulador Lambda que queremos testar.
O manipulador Lambda receberá a entrada, verificará a configuração, validará a entrada e chamará a camada lógica para criar o pedido. Aqui está um trecho da assinatura do manipulador:
Clique aqui para ver o código completo do manipulador.
Agora, vamos começar a escrever o teste de integração.
No pytest do Python, podemos usar arquivos conftest para definir fixtures que são executados antes de qualquer módulo de teste e definir simulações globais ou variáveis de ambiente que nosso manipulador Lambda requer.
Definimos várias variáveis de ambiente do manipulador que os sinalizadores de logger, tracer e feature precisam. Além disso, a linha 18 define a variável para o nome da tabela do DynamoDB na qual salvamos os pedidos. No código do CDK que define a tabela, defini o nome da tabela como uma saída de pilha do CloudFormation para que ela possa ser carregada como uma variável de ambiente no teste de forma fácil. É um truque legal, e recomendo que você faça isso para todas as variáveis de ambiente que precisa carregar nos testes de integração.
Na linha 21, criamos um fixture que injetará o nome da tabela do DynamoDB como um argumento para nosso teste de manipulador.
Agora, vamos dar uma olhada em alguns dos testes de integração deste manipulador.
Veja o primeiro teste de fluxo feliz - 'test_handler_200_ok' na linha 10.
Quando o manipulador de pedidos de criação recebe um evento válido, esperamos que ele o grave na tabela do DynamoDB e retorne o código HTTP 200 OK.
A linha 13 cria um exemplo de carga útil de entrada válida da API.
Na linha 14, acionamos o manipulador Lambda create_order com um evento gerado que contém a entrada válida e os outros atributos de metadados do API Gateway. Agora podemos adicionar pontos de interrupção ao manipulador, depurar nossa lógica e garantir que os testes sejam aprovados.
O método de fábrica de geração de eventos 'generate_api_gw_event' cria um evento completo do AWS API Gateway com a carga útil de teste e pode ser encontrado aqui .
Uma vez concluído com sucesso, o teste afirma nas linhas 16 a 20 que o esquema de resposta é válido e contém os valores esperados.
Nas linhas 22-26, obtemos o item inserido da tabela do DynamoDB e verificamos se a função escreveu o item corretamente na tabela. O nome da tabela foi preenchido como um argumento para o teste (como vimos no fixture conftest 'table_name').
Serviços reais da AWS vs. simulações
Uma vantagem significativa que ganhamos ao executar os testes localmente com o Pytest, mas contra recursos reais da AWS, é que podemos simular quase tudo. No segundo teste, 'test_internal_server_error', simulamos o recurso AWS boto Table e simulamos um erro de cliente do DynamoDB quando falhamos em salvar um item no banco de dados. Essa simulação nos permite testar nosso código de repetição e estratégia de fila de letras mortas e verificar se o valor de retorno da função, nesse caso, é HTTP 500 .
Na linha 33, simulamos a função interna na camada lógica da função que cria um recurso de tabela 'boto'. A função simulada levantará uma exceção quando chamada.
A linha 37 afirma que a exceção foi tratada corretamente e, na linha 38, afirmamos que nosso objeto de função simulada foi chamado para garantir que foi ele quem gerou a exceção.
Podemos escolher simular qualquer lógica interna que desejamos quebrar. Podemos usar recursos reais da AWS e simular apenas alguns deles, dependendo da sua lógica. Por fim, você desejaria cobrir todos os casos de uso em que lida com exceções ou erros e simula chamadas de API com falhas.
Utilitários de cobertura de código podem ajudar você a garantir que você cubra suas bases. No entanto, eles não garantem que seu manipulador realmente funcione. Você deve simular casos de uso de negócios reais.
Veja o teste de integração completo aqui .
Testes de ponta a ponta (E2E)
Os testes de ponta a ponta visam ser executados em recursos implantados, simular casos de uso de clientes reais e acionar processos orientados a eventos em toda a sua arquitetura.
Você quer garantir que sua infraestrutura esteja configurada corretamente, que o evento percorra os recursos da AWS corretamente, que suas funções do AWS Lambda sejam executadas com variáveis de ambiente corretas e que suas funções estejam configuradas com todas as permissões necessárias.
Geraremos localmente no IDE o evento inicial e verificaremos as respostas.
A partir daí, todo o processo é executado na sua conta AWS, e não temos controle algum sobre ele. Como tal, esses são os testes mais lentos para executar, pois testam toda a cadeia do início ao fim na infraestrutura.
Observe que não temos nenhuma opção para simular falhas, então eu recomendo testar apenas fluxos de clientes satisfeitos e testes relacionados à segurança (mais sobre isso depois).
Não pesquisaremos ou chamaremos recursos da AWS diretamente, mas usaremos chamadas de API, da mesma forma que o cliente faria. Devemos enviar uma chamada de API REST para o API Gateway e afirmar sua resposta. Qualquer efeito colateral já foi testado e provou estar funcionando nos testes de integração que usam serviços REAIS da AWS, então não há necessidade de testá-lo novamente, exceto para afirmar a resposta do Lambda. Para colocá-lo em contexto, no serviço de pedidos, ao criar um novo pedido, verifique se a resposta é válida e contém valores conforme o esperado. No entanto, não verifique a tabela do DynamoDB diretamente para o item inserido, mas use APIs REST voltadas para o cliente - uma API 'obter pedido' (ela ainda não existe no meu exemplo, mas você entendeu) para verificar se o item foi inserido.
Na terceira parte desta série, discutirei como testar Step Functions e serviços assíncronos, mas, por enquanto, vamos nos concentrar no fluxo síncrono do serviço de pedidos.
Exemplos de testes E2E
Vamos dar uma olhada nos testes de ponta a ponta abaixo:
Na linha 15, iniciamos o fluxo feliz de um usuário criando uma solicitação de pedido válida.
Encontramos a URL completa do serviço com o mecanismo de saída de pilha que fizemos no teste de integração para o nome da tabela.
A linha 17 gera a carga útil de entrada válida.
A linha 18 envia uma solicitação POST REST API para o API Gateway.
A linha 19 afirma o código de resposta da função, e as linhas 21 a 23 afirmam os dados de resposta.
Na linha 26, testamos o tratamento correto de entradas inválidas.
Enviamos na linha 28 uma carga malformada (não corresponde ao esquema) e esperamos em
linhas 29-31 para obter um código de status HTTP BAD REQUEST com um corpo JSON vazio.
Veja o teste completo de ponta a ponta aqui .
Depuração de testes E2E
Os testes de integração podem ser aprovados, mas a variação E2E do teste pode falhar devido a permissões de função mal configuradas, importações ausentes no pacote ZIP da função Lambda, variáveis de ambiente ausentes e outros casos de uso "divertidos".
A única maneira de depurá-los é abrir os bons e velhos logs do AWS CloudWatch, visualizar o erro, implantar uma versão corrigida e executar o teste novamente.
Quer aprender as melhores práticas de registro de funções Lambda? Confira meu post aqui .
Testes de Segurança
É essencial testar seu mecanismo de autenticação e autorização. Normalmente, esses mecanismos são implementados com um autorizador Lambda personalizado, autorização IAM, autorizador Cognito ou código personalizado no manipulador de função que faz ambos.
Esses mecanismos (todos, exceto o código da função personalizada) são configurados na parte IaC (CDK, SAM, etc.) e é fundamental garantir que eles estejam configurados corretamente e não tenham sido excluídos acidentalmente.
Então, é importante invocar a função com permissões inválidas e garantir que a função/API Gateway retorne a resposta HTTP 40X correta. Seria melhor se você simulasse os seguintes casos de uso:
Chame sua função com um token inválido (token expirado).
Chame sua função com um token de autenticação válido (faça login como usuário de teste), mas com permissões inválidas (o usuário não tem permissão para executar a API, mas está conectado ao sistema).
Observe que não incluí nenhum mecanismo de autenticação/autorização no meu serviço de "pedido" de exemplo, pois isso complicaria o exemplo.
Leia mais sobre isso aqui .
Testes de desempenho
Monitorar o desempenho do seu serviço sem servidor e ajustá-lo do ponto de vista de custo/desempenho em relação ao tráfego esperado de clientes é essencial.
Seria melhor executar esses testes ocasionalmente e pelo menos uma vez antes da produção do GA. Esses testes fornecem insights sobre gargalos de serviço e conexões ocultas e permitem que você configure melhores valores de simultaneidade reservados ou provisionados para seu serviço.
É recomendado utilizar ferramentas como AWS X-Ray , AWS Lambda Power Tuning e AWS Lambda Powertools tracer utility. Leia mais sobre isso aqui .
Você pode encontrar mais tarefas de preparação para produção sem servidor na minha postagem aqui .
A Pirâmide de Testes e o Pipeline CI/CD
Meu pipeline de CI/CD sem servidor recomendado executará testes de unidade e infraestrutura, depois implantará o aplicativo na AWS e executará testes de integração e e2e.
A falha em qualquer uma das etapas atua como um portão que faz com que todo o pipeline falhe e o impede de prosseguir para a próxima etapa.
Para um pipeline de CI/CD sem servidor baseado em ações do GitHub e CDK, leia minha postagem aqui .
Resumo - Por que isso funciona?
Listei os desafios de teste sem servidor na parte um da série. Seguindo as diretrizes que apresentei lá e implementando a pirâmide de teste sem servidor, conseguimos mitigar a maioria, se não todos, dos desafios de teste sem servidor:
Oferecemos uma boa experiência ao desenvolvedor; podemos executar o teste a partir do IDE e depurar localmente com integração e teste de unidade.
Nós automatizamos todos os nossos testes.
Ganhamos confiança de que nosso código funcionará no E2E porque usamos serviços reais da AWS mesmo em testes de integração.
Testamos tanto nossa configuração de infraestrutura quanto o código de serviço nos testes.
Abordamos aspectos de infraestrutura, desempenho e custo em nossos testes.
Nós administramos toda a cadeia de eventos orientada por eventos, do começo ao fim.
Simulamos falhas tanto em nossa lógica quanto em chamadas de API da AWS.
Abordamos aspectos de validação de entrada.