Deslocar teste para a esquerda com testes de unidade

O teste ajuda a garantir que o código seja executado conforme esperado, mas o tempo e o esforço para criar testes consomem tempo que poderia ser dedicado a outras tarefas, como o desenvolvimento de recursos. Em virtude desse custo, é importante extrair o máximo de valor dos testes. Este artigo discute os princípios de teste do DevOps, focando o valor do teste de unidade e uma estratégia de teste de deslocamento para a esquerda.

Testadores dedicados costumavam escrever a maioria dos testes, e muitos desenvolvedores de produtos não aprenderam a escrever testes de unidade. Pode parecer difícil ou dar muito trabalho escrever testes. Alguns são céticos quanto à possibilidade da estratégia de teste de unidade funcionar devido a experiências ruins com testes de unidade mal escritos ou por medo de que os testes de unidade substituam os testes funcionais.

Graphic that describes arguments about adopting unit testing.

Para implementar uma estratégia de teste do DevOps, seja pragmático e concentre-se na criação de dinamismo. Embora você possa preferir testes de unidade para código novo ou existente com possibilidade de refatoração correta, talvez faça sentido para uma base de código herdada permitir um certo nível de dependência. Se partes significativas do código do produto usarem SQL, permitindo a dependência de testes de unidade do provedor de recursos SQL em vez de usar simulação, essa camada poderá ser uma abordagem de curto prazo para o progresso.

À medida que as organizações do DevOps amadurecem, a liderança consegue aprimorar processos mais facilmente. Apesar da possível resistência à mudança, as organizações do Agile valorizam os benefícios diretos de mudanças. Deve ser fácil vender a visão de testes mais rápidos e com menos falhas, pois isso significa mais tempo para investir na geração de novo valor por meio do desenvolvimento de recursos.

Taxonomia de teste do DevOps

Definir uma taxonomia de teste é um aspecto importante do processo de teste do DevOps. Uma taxonomia de teste do DevOps classifica testes individuais por meio de dependências e do tempo de execução. Os desenvolvedores devem entender os tipos certos de testes a serem usados em cenários distintos e quais testes são exigidos por partes distintas do processo. A maioria das organizações categoriza os testes em quatro níveis:

  • Os testes L0 e L1 são testes de unidade, ou testes que dependem do código na montagem em teste e nada mais. L0 é uma classe ampla de testes de unidade rápidos na memória.
  • L2 são testes funcionais que podem exigir o assembly mais outras dependências, como SQL ou o sistema de arquivos.
  • Os testes L3 são funcionais executados em implantações de serviço testáveis. Essa categoria de teste exige uma implantação de serviço, mas pode usar stubs para as principais dependências de serviço.
  • Os testes L4 são uma classe restrita de testes de integração executados em produção. Os testes L4 exigem a implantação total do produto.

O ideal seria que todos os testes fossem executados o tempo todo, mas isso não é viável. As equipes podem selecionar onde executar cada teste no processo do DevOps e usar a estratégia shift-left ou shift-right para mover diferentes tipos de teste mais cedo ou mais tarde no processo.

Por exemplo, a expectativa pode ser que os desenvolvedores sempre executem testes L2 antes de confirmar, uma solicitação pull falhe automaticamente se a execução de teste L3 falhar e a implantação possa ser bloqueada se os testes L4 falharem. As regras específicas variam de acordo com a organização, mas impor as expectativas para todas as equipes de uma organização move todos em direção às mesmas metas de visão de qualidade.

Diretrizes de teste de unidade

Defina diretrizes rígidas para testes de unidade L0 e L1. Esses testes precisam ser bem rápidos e confiáveis. Por exemplo, o tempo médio de execução por teste L0 em um assembly deve ser inferior a 60 milissegundos. O tempo médio de execução por teste L1 em um assembly deve ser inferior a 400 milissegundos. Nenhum teste neste nível deve exceder 2 segundos.

Uma equipe da Microsoft executa mais de 60.000 testes de unidade em paralelo em menos de seis minutos. Seu objetivo é reduzir esse tempo para menos de um minuto. A equipe controla o tempo de execução do teste de unidade com ferramentas como o gráfico a seguir e arquiva bugs em testes que excedem o tempo permitido.

Chart that shows continuous focus on test execution time.

Diretrizes de teste funcional

Testes funcionais devem ser independentes. O conceito-chave para testes L2 é o isolamento. Testes corretamente isolados podem ser executados de forma confiável em qualquer sequência, pois eles têm controle total do ambiente em que são executados. No início do teste, deve aparecer o estado. Se um teste criar dados e os deixar no banco de dados, isso poderá corromper a execução de outro teste que dependa de um estado de banco de dados distinto.

Testes herdados que precisam de uma identidade de usuário podem ter chamado provedores de autenticação externos para obter a identidade. Essa prática apresenta vários desafios. A dependência externa pode ficar momentaneamente não confiável ou indisponível, interrompendo o teste. Essa prática também viola o princípio de isolamento de teste porque um teste pode alterar o estado de uma identidade, como permissão, resultando em um estado padrão inesperado para outros testes. Considere evitar esses problemas investindo no suporte de identidade na estrutura de teste.

Princípios de teste do DevOps

Para ajudar na transição de um portfólio de testes para processos modernos do DevOps, articule uma visão de qualidade. As equipes devem aderir aos princípios de teste a seguir quando definem e implementam uma estratégia de teste do DevOps.

Diagram that shows an example of a quality vision and lists test principles.

Desloque para a esquerda para testar antes

A execução dos testes pode ser demorada. Há um crescimento substancial de quantidades e tipos de testes que acompanha a maior escala de projetos. Quando os conjuntos de testes levam horas ou dias para serem concluídos, eles podem demorar ainda mais até a execução no último momento. Você só percebe os benefícios da qualidade de código de testes muito depois da confirmação do código.

Testes de longa duração também podem gerar falhas cuja investigação é demorada. As equipes podem criar uma tolerância a falhas, em especial no início de sprints. Essa tolerância destorce o valor de testes com insights sobre a qualidade da base de código. Testes de longa duração e de última hora também agregam imprevisibilidade às expectativas de fim de sprint porque um valor de dívida técnica desconhecido deve ser pago para que o código seja enviável.

A meta do teste shift-left é mover a qualidade upstream ao executar tarefas de teste antes no pipeline. Com uma combinação de aprimoramentos de teste e processo, o shift-left reduz o tempo de execução de testes e o impacto de falhas posteriormente no ciclo. Deslocar para a esquerda garante a conclusão da maioria dos testes antes que uma alteração seja mesclada na ramificação principal.

Diagram that shows the move to shift-left testing.

Além de deslocar certas responsabilidades de teste para a esquerda para melhorar a qualidade do código, as equipes podem deslocar outros aspectos de teste para a direita, ou mais adiante no ciclo do DevOps, para aprimorar o produto final. Para saber mais, confira Desloque para a direita para testar em produção.

Escreva testes no nível mais baixo possível

Escreva mais testes de unidade. Privilegie testes com o menor número de dependências externas e concentre-se na execução da maioria dos testes como parte da compilação. Considere um sistema de compilação paralela capaz de executar testes de unidade para um assembly assim que o assembly e testes associados caem. Não é viável testar todos os aspectos de um serviço nesse nível, mas o princípio aplicado é usar testes de unidade mais leves quando conseguem gerar os mesmos resultados que testes funcionais mais pesados.

Busque a confiabilidade de testes

É caro manter um teste não confiável em termos de organização Esse teste não beneficia a meta de eficiência de engenharia, o que dificulta a tentativa de fazer mudanças com confiança. Os desenvolvedores devem ser capazes de fazer alterações em qualquer lugar, com a confiança de que nada foi interrompido. Mantenha um alto nível de confiabilidade. Desestimule o uso de testes de interface de usuário porque eles tendem a não ser confiáveis.

Escreva testes funcionais que possam ser executados em qualquer lugar

Os testes podem usar pontos de integração especializados que visem especificamente habilitar o teste. Uma das razões para essa prática é a falta de capacidade de teste no próprio produto. Infelizmente, testes como esses costumam depender de conhecimento interno e usam detalhes de implementação que não são relevantes de uma perspectiva de teste funcional. Esses testes se limitam a ambientes com os segredos e a configuração necessários para executar os testes, o que costuma excluir implantações de produção. Os testes funcionais devem usar apenas a API pública do produto.

Projetar produtos para capacidade de teste

As organizações em um processo do DevOps em desenvolvimento têm uma visão completa do significado de entregar um produto de qualidade em uma cadência da nuvem. Mudar o saldo em favor do teste de unidade e não do teste funcional exige que as equipes façam escolhas de design e implementação que sustentem a capacidade de teste. Existem variadas ideias sobre o que constitui um código com projeto e implementação adequados para ter estabilidade, assim como existem estilos de codificação distintos. O princípio básico é que o projeto para ter capacidade de teste deve se tornar parte principal da discussão sobre design e qualidade do código.

Tratar o código de teste como código do produto

Ao declarar explicitamente que o código de teste é o código do produto, você deixa claro que as qualidades do código de teste e do código do produto são igualmente importantes para o envio. As equipes devem dar tratamento igual ao código de teste e ao código do produto, e ter o mesmo nível de cuidado na criação e na implementação de testes e estruturas de teste. Esse esforço se assemelha ao gerenciamento de configuração e infraestrutura como código. Para ser completa, uma revisão de código deve considerar o código de teste e mantê-lo no mesmo nível de qualidade que o código do produto.

Usar infraestrutura de teste compartilhada

Não seja tão exigente ao usar a infraestrutura de teste para gerar sinais de qualidade confiáveis. Encare o teste como um serviço compartilhado para a equipe inteira. Armazene o código de teste de unidade ao lado do código do produto e compile-o com o produto. Os testes executados como parte do processo de compilação também devem ser executados em ferramentas de desenvolvimento, como o Azure DevOps. Se for possível executar os testes em todos os ambientes, desde o desenvolvimento local até a produção, eles terão a mesma confiabilidade que o código do produto.

Torne os proprietários de código responsáveis pelos testes

O código de teste deve residir ao lado do código do produto em um repositório. Para que o código seja testado em um limite de componente, transfira a responsabilidade pelo teste para a pessoa que está escrevendo o código do componente. Não confie em outras pessoas para testar o componente.

Estudo de caso: deslocamento à esquerda com testes de unidade

Uma equipe da Microsoft decidiu substituir os conjuntos de testes herdados por testes de unidade modernos do DevOps e um processo de deslocamento à esquerda. A equipe acompanhou o progresso em sprints trissemanais, conforme mostrado no gráfico a seguir. O gráfico abrange os sprints 78-120, que representam 42 sprints em 126 semanas, ou em torno de dois anos e meio de esforço.

A equipe começou com 27.000 testes herdados no sprint 78, e atingiu zero testes herdados em S120. Um conjunto de testes unitários L0 e L1 substituiu a maioria dos testes funcionais anteriores. Novos testes L2 substituíram alguns dos testes, e muitos dos testes antigos foram excluídos.

Diagram that shows a sample test portfolio balance over time.

Em uma jornada de software que leva mais de dois anos para ser concluída, há muito o que aprender no próprio processo. Em geral, o esforço para refazer totalmente o sistema de teste em dois anos foi um investimento em massa. Nem todas as equipes de recursos realizaram o trabalho simultaneamente. Muitas equipes na organização inteira investiram tempo em cada sprint, e em alguns sprints, a equipe dedicou a maior parte do tempo. Embora seja difícil medir o custo da mudança, este foi um requisito inegociável para atingir as metas de qualidade e desempenho da equipe.

Introdução

No início, a equipe abandonou os antigos testes funcionais, chamados de testes TRA. A equipe buscava a adesão de desenvolvedores à ideia de escrever testes de unidade, especialmente para novos recursos. O foco era facilitar ao máximo a realização de testes L0 e L1. A equipe precisava desenvolver essa capacidade primeiro e ganhar força.

O gráfico anterior mostra que houve um aumento precoce na contagem de testes de unidade, pois a equipe percebeu o benefício de criar testes de unidade. Era fácil manter e mais rápido de executar testes de unidade, e eles tinham menos falhas. Foi fácil obter suporte para executar todos os testes de unidade no fluxo de solicitação de pull.

A equipe não se concentrou em escrever novos testes L2 até o sprint 101. Enquanto isso, a contagem de testes TRA caiu de 27.000 para 14.000 do Sprint 78 para o Sprint 101. Novos testes de unidade substituíram alguns dos testes TRA, mas muitos foram excluídos, com base na análise da equipe sobre sua utilidade.

Os testes TRA saltaram de 2100 para 3800 no sprint 110 porque foram descobertos mais testes na árvore de origem e eles adicionados ao gráfico. Perceberam que os testes sempre funcionavam, mas eram rastreados incorretamente. Não foi uma crise, mas era importante ser honesto e reavaliar conforme necessário.

Como agilizar processos

Assim que a equipe obteve um sinal de integração contínua (CI) extremamente rápido e confiável, ele se tornou um indicador confiável da qualidade do produto. A captura de tela a seguir mostra a solicitação de pull e o pipeline de CI em ação, e o tempo necessário para passar por várias fases.

Diagram that shows the pull request and rolling CI pipeline in action.

Leva cerca de 30 minutos para passar da solicitação de pull para a mesclagem, incluindo a execução de 60.000 testes de unidade. Da mesclagem de código até a compilação de CI leva em torno de 22 minutos. O primeiro sinal de qualidade da CI, SelfTest, vem depois de cerca de uma hora. A maioria dos produtos são testados com a alteração proposta. Duas horas após a migração para o SelfHost, todo o produto é testado e a alteração está pronta para entrar em produção.

Usando métricas

A equipe acompanha um scorecard como o exemplo a seguir. Em alto nível, o scorecard rastreia dois tipos de métricas: saúde ou dívida e velocidade.

Diagram that shows a metrics scorecard for tracking test performance.

Para métricas de integridade do site em tempo real, a equipe controla o tempo para detectar, o tempo para mitigar e o número de itens de reparo carregado por uma equipe. Um item de reparo é o trabalho que a equipe identifica em uma retrospectiva de site ao vivo para evitar que ocorram incidentes similares. O scorecard também rastreia se as equipes estão fechando os itens de reparo em um prazo razoável.

Para métricas de integridade de engenharia, a equipe rastreia bugs ativos por desenvolvedor. Se uma equipe tiver mais de cinco bugs por desenvolvedor, ela deverá priorizar a correção desses bugs antes do desenvolvimento de novos recursos. A equipe também rastreia bugs antigos em categorias especiais, como segurança.

As métricas de velocidade de engenharia medem a velocidade em partes distintas do pipeline de integração e entrega contínuas (CI/CD). O objetivo geral é aumentar a velocidade do pipeline do DevOps: partir de uma ideia, colocar o código em produção e receber o retorno de dados de clientes.

Próximas etapas