Microsserviço 9
O mundo dos testes avançou significativamente nos últimos anos, mas testar funcionalidades em nosso código de modo eficaz e eficiente continua sendo um desafio em sistemas distribuídos.
Entender os diferentes tipos de testes que podemos executar é importante para encontrar o equilíbrio entre forças ocasionalmente opostas: entregar o software em produção o mais rápido possível e garantir que tenha qualidade suficiente.
Podemos classificar os testes como:
- testes de unidade: construímos o software corretamente?
- testes de aceitação: construímos o software correto?
- testes exploratórios: como posso causar falhas no sistema?
- testes de propriedades: tempo de resposta, escalabilidade, desempenho, segurança.
A grande maioria desses testes tem o foco de validar o software antes de entrar em produção. A quantidade exata de cada tipo dependerá muito do contexto, da complexidade e da natureza do sistema. Mas, caso os testes estejam sendo feitos manualmente, é melhor mudar esse cenário antes de se direcionar aos microsserviços, para que seja possível validar o software com rapidez e eficiência. Logo, utilizar um conjunto de ferramentas que permitam automatizar processos manuais é de extrema importância.
Óbvio que isso não deve ser encarado como “não ter testes manuais”, mas sim como eliminar tarefas repetitivas. Testes manuais estão mais relacionados a descobertas e podem ser utilizados quando o custo de escrever um teste automatizado se torna impraticável.
Escopo dos testes
O modelo original de Cohn separa os testes em:
- Testes de UI;
- Testes de serviço;
- Testes de unidade.
Olhando de baixo para cima, à medida que subimos, o escopo do teste aumenta. Olhando de cima para baixo, à medida que descemos, temos testes sendo executados mais rapidamente, com um ciclo de feedback mais curto.
Teste de unidade
Testa uma unidade do sistema, por exemplo, uma chamada de função. O TDD é uma prática que se enquadra nessa categoria. Geralmente, a maior quantidade de testes está nessa categoria, já que são extremamente rápidos, possuem feedback curto, permitem a refatoração com mais segurança e conseguem capturar a maioria dos bugs.
Testes de serviço
Ignoram a interface do usuário e testam diretamente o microsserviço. A falha no teste ficará limitada somente ao microsserviço em teste e, para isso, isolamos componentes externos com stubs para que somente o microsserviço em questão esteja no escopo.
A ideia de stubs é ter um serviço que envia respostas prontas para as requisições conhecidas feitas pelo microsserviço em teste, não importando a quantidade de requisições feitas. Um stub pode ser um serviço simples iniciado pela linha de comando.
Existem também os mocks que, diferente dos stubs, simulam o comportamento de um item. Logo, possuem um escopo maior do que o teste de unidade.
Teste fim a fim
São os testes executados em todo o sistema, simulando o comportamento do usuário. O ponto de atenção com esse tipo de teste é o quão custoso ele pode ser, já que podemos precisar implantar vários microsserviços juntos e, então, executar um teste que envolva todos eles.
Devemos encontrar o equilíbrio entre fazer ou não esse tipo de teste, já que, como são testes mais demorados, afetam diretamente o feedback e a produtividade dos desenvolvedores. A correção de qualquer falha será mais lenta e diminuirá a capacidade de lançar alterações pequenas. Nesse cenário, podem acabar trazendo mais danos do que vantagens.
Abordagem
O que estamos procurando com os diferentes tipos de testes é chegar a um equilíbrio razoável, já que queremos feedback rápido e confiança de que nosso sistema funciona como esperado.
Uma abordagem para o escopo de teste seria: caso um teste de escopo maior falhe, escrever um teste com escopo menor para encontrar mais rápido o problema. Vale a pena substituir testes de escopo maior por testes de escopo menor sempre que possível.
Agora, à medida que o escopo dos testes aumenta, também aumenta a quantidade de partes envolvidas. Essas partes envolvidas podem introduzir falhas nos testes que não mostram um erro na funcionalidade em si, mas sinalizam que algum outro problema ocorreu. Por exemplo: executamos os testes, mas algum dos microsserviços envolvidos está inativo. Receberemos uma falha que não possui relação com a natureza do teste em questão.
Quanto mais partes envolvidas, mais frágeis e menos determinísticos poderão ser os nossos testes. Testes que falham “de vez em quando” são testes frágeis. Devemos nos esforçar ao máximo para eliminar esse tipo de comportamento, caso contrário perderemos a confiança na suíte de testes e chegaremos à normalização do desvio, já que, com o tempo, podemos ficar acostumados com algo errado a ponto de começarmos a aceitá-lo como normal, e não como um problema.
Assim, caso não seja possível corrigi-los, é melhor removê-los da suíte de testes para poder lidar com eles de outra forma.
Ambientes de teste não deveriam ser compartilhados. O ideal é que cada contexto tenha seu próprio ambiente de teste.
Metaversão
Como temos vários serviços relacionados e que funcionam juntos, pode surgir a ideia de versionar todos os sistemas com o mesmo número.
O problema é que, com isso, aceitamos a ideia de que alterar e implantar vários serviços de uma só vez é aceitável. Com isso, aumentaremos o acoplamento dos serviços que antes estavam separados, tornando-os cada vez mais entrelaçados.
A implantação se tornará um caos, pois teremos de coordenar a implantação de vários microsserviços de uma só vez. Exatamente o oposto do que queremos ao adotar microsserviços: implantações independentes, autonomia para as equipes e lançamentos de software com mais eficácia.
No final, o que estamos procurando é garantir que, quando implantarmos um novo serviço em produção, nossas mudanças não causarão falhas aos consumidores.
Schemas
Os schemas podem até ajudar a identificar incompatibilidades estruturais, mas não ajudam a identificar as incompatibilidades semânticas, isto é, alterações no comportamento que causem falhas.
Para esse cenário, precisamos de testes de contrato e de contratos orientados a consumidores.
Testes de contrato e CDC (Consumer-Driven Contracts)
O propósito dos testes de contrato é que uma equipe cujo microsserviço consome um serviço externo escreva os testes que descrevem como ela espera que esse serviço externo vá se comportar.
Essa estratégia também pode ser usada pelo provedor do serviço externo, criando um cliente que consuma o próprio serviço. Assim, no momento do build, caso aconteça alguma falha no serviço cliente, ficará evidente que os consumidores externos serão impactados. Com isso, duas abordagens podem ser adotadas: ou se corrige o problema, ou se inicia uma discussão sobre a introdução de uma alteração que causará incompatibilidade.
Um exemplo de ferramenta que pode ser usada para CDC (Consumer-Driven Contracts) é o Pact (ferramenta de testes orientada a consumidores).
Excesso de microsserviços
À medida que o número de microsserviços aumenta, os desenvolvedores precisarão trabalhar com uma quantidade cada vez maior deles, e a experiência dos desenvolvedores pode começar a sofrer, pelo simples fato de haver a necessidade de executar localmente mais e mais microsserviços.
O ponto é que alguns conjuntos de tecnologia exigem mais recursos já de partida — por exemplo, microsserviços baseados em JVM. Entretanto, outros conjuntos de tecnologia podem resultar em microsserviços com menor uso de recursos ou inicialização mais rápida, talvez permitindo executar muito mais microsserviços localmente.
O ideal é executar no ambiente local somente os microsserviços que realmente forem necessários para trabalhar. Caso o time seja responsável por 6 microsserviços, o desenvolvedor deve ser capaz de executar esses 6 microsserviços da forma mais eficaz possível. Assim, caso algum desses microsserviços faça chamadas externas que estejam fora do escopo do time, stubs precisam ser criados.
Os únicos microsserviços reais que devem ser executados localmente são aqueles nos quais o desenvolvedor trabalha.
Testes em produção
O foco principal dos testes é executar uma série de modelos para nos dar segurança e garantir que o sistema funciona e se comporta como esperado, tanto do ponto de vista funcional quanto do ponto de vista não funcional. Gostaríamos de saber se há algum problema com nosso software antes que um usuário final experimente o problema.
Mesmo com esses testes antes da implantação, não é possível reduzir as chances de falha a zero.
Podemos e devemos aplicar testes em ambiente de produção. Isso pode ser feito de forma segura e fornecerá um feedback de melhor qualidade do que os testes em pré-produção. Os tipos de teste que podemos executar em produção são:
- ping: verificar se o serviço está ativo;
- smoke test: executado como parte das atividades de implantação e que garante que o software implantado funciona corretamente;
- canary release: lança uma nova versão do software para uma pequena parcela dos usuários com o intuito de “testar” se funciona corretamente.
Claro que, ao decidir fazer testes em produção, é importante que eles não causem problemas no ambiente, seja gerando instabilidade, seja corrompendo os dados. Além disso, devem existir meios de realizar um rollback rápido caso algum problema seja evidenciado no ambiente de produção, a fim de reduzir o impacto para os clientes.
Testes multifuncionais
Além dos testes funcionais, que dizem respeito às funcionalidades do sistema, também temos que realizar os testes não funcionais, que são características do microsserviço que não podem ser implementadas simplesmente como uma funcionalidade. Por exemplo:
- latência aceitável;
- número de usuários suportados;
- nível de acessibilidade;
- nível de proteção dos dados dos clientes.
Requisitos não funcionais devem ser analisados o mais cedo possível e revisados com regularidade.
Muitos deles só serão atendidos plenamente no ambiente de produção, mas podemos definir estratégias de teste que ajudem a verificar se estamos na direção correta, a fim de atender a esses requisitos não funcionais.
Testes de desempenho
Identificar as causas de latência é extremamente importante. Em uma cadeia de chamadas, se uma parte dessa cadeia ficar lenta, tudo ficará lento.
Verificar as principais jornadas do microsserviço é essencial. Para obter resultados relevantes, cenários de testes com usuários simulados são uma abordagem possível, observando o desempenho à medida que o número de usuários aumenta. A partir dos resultados, gargalos podem ser identificados e correções, aplicadas.
Testes de robustez
Robustez está relacionada a aumentar a confiabilidade do sistema, identificando o elo mais fraco. Podemos atingir esse objetivo, por exemplo, executando o microsserviço atrás de um balanceador de carga para tolerar a falha de uma instância, usando circuit breakers e recriando determinadas falhas para garantir que o microsserviço continuará funcionando como um todo.
Conclusão
Testar sistemas distribuídos e baseados em microsserviços exige mais do que simplesmente aumentar a quantidade de testes: é necessário compreender o escopo adequado para cada tipo de teste, automatizar ao máximo o que é repetitivo e para descoberta, utilizar testes manuais. O equilíbrio entre testes de unidade, de serviço, fim a fim, testes de contrato e testes em produção é o que sustenta a confiança na qualidade do microsserviço sem comprometer a agilidade de entrega.
Evitar metaversão e acoplamentos desnecessários, investir em Consumer-Driven Contracts, limitar o número de microsserviços executados localmente e planejar testes multifuncionais (desempenho, robustez, segurança, acessibilidade) ajuda a manter a arquitetura sustentável.
Em última análise, a estratégia de testes em microsserviços deve apoiar a evolução contínua do sistema: feedback rápido, diagnósticos precisos e a capacidade de mudar com segurança, sem sacrificar a experiência dos usuários nem a autonomia das equipes.
