Uma estratégia eficaz para teste de código automatizado é extremamente importante para garantir o trabalho rápido e de alta qualidade das equipes de programadores envolvidas no suporte e desenvolvimento de projetos da web. O autor do artigo diz que na empresa
StackPath , na qual ele trabalha, agora tudo está funcionando
bem com os testes. Eles têm muitas ferramentas para verificar o código. Mas a partir dessa variedade, você precisa escolher o que é mais adequado para cada caso. Este é um problema separado. E depois que as ferramentas necessárias forem selecionadas, você ainda precisará tomar uma decisão sobre a ordem de uso.

O autor do artigo diz que o StackPath está satisfeito com o nível de confiança na qualidade do código que foi alcançado graças ao sistema de teste aplicado. Aqui ele deseja compartilhar uma descrição dos princípios de teste desenvolvidos pela empresa e falar sobre as ferramentas utilizadas.
Princípios de Teste
Antes de falar sobre ferramentas específicas, vale a pena pensar na resposta à pergunta sobre o que são bons testes. Antes de iniciar o trabalho em nosso
portal para clientes, formulamos e escrevemos os princípios que gostaríamos de seguir ao criar testes. O que fizemos em primeiro lugar é exatamente o que nos ajudou na escolha das ferramentas.
Aqui estão os quatro princípios em questão.
Number Princípio número 1. Os testes devem ser entendidos como tarefas de otimização
Uma estratégia de teste eficaz é resolver o problema de maximizar um determinado valor (nesse caso, o nível de confiança de que o aplicativo funcionará corretamente) e minimizar certos custos (aqui os "custos" são representados pelo tempo necessário para dar suporte e executar os testes). Ao escrever testes, geralmente fazemos as seguintes perguntas relacionadas ao princípio descrito acima:
- Qual é a probabilidade de que este teste encontre um erro?
- Esse teste melhora nosso sistema de teste e os custos dos recursos necessários para escrevê-lo valem os benefícios derivados?
- É possível obter o mesmo nível de confiança na entidade que está sendo testada que esse teste fornece, criando outro teste que é mais fácil de escrever, manter e executar?
▍ Princípio nº 2. O uso excessivo de mox deve ser evitado.
Uma das minhas explicações favoritas do conceito de "mok" foi dada
nesta apresentação na conferência Assert.js. 2018. O orador abriu a questão mais profundamente do que eu vou abrir aqui. No discurso, a criação de mokas é comparada com "furos na realidade". E acho que essa é uma maneira muito visual de perceber os moks. Embora existam mokas em nossos testes, comparamos a diminuição no “custo” dos testes que os mokas fornecem devido à simplificação do processo de gravação e execução de testes, com a diminuição no valor dos testes que causam outro buraco na realidade.
Anteriormente, nossos programadores dependiam muito de testes de unidade escritos para que todas as dependências filhas fossem substituídas por mokas usando a API de renderização
enzimática superficial. As entidades renderizadas dessa maneira foram verificadas usando os instantâneos
Jest . Todos esses testes foram escritos usando um padrão semelhante:
it('renders ', () => { const wrapper = shallow();
Esses testes são preenchidos com a realidade em muitos lugares. Essa abordagem facilita muito a cobertura de código 100% com testes. Ao escrever esses testes, você deve pensar muito pouco, mas se você não verificar todos os inúmeros pontos de integração, esses testes não serão particularmente valiosos. Todos os testes podem ser concluídos com êxito, mas isso não dá muita confiança na operacionalidade do aplicativo. E pior ainda, todos os mokas têm um "preço" oculto que você deve pagar após a realização dos testes.
▍ Princípio nº 3. Os testes devem facilitar a refatoração do código, não complicá-lo.
Testes como o mostrado acima complicam a refatoração. Se eu achar que em muitos lugares do projeto há código duplicado, e depois de um tempo eu formatar esse código como um componente separado, todos os testes para os componentes nos quais eu usarei esse novo componente falharão. Os componentes derivados da técnica de renderização superficial já são outra coisa. Onde eu costumava ter marcações repetidas, agora existe um novo componente.
A refatoração mais complexa, que envolve adicionar alguns componentes a um projeto e remover outros componentes, leva a ainda mais confusão. O fato é que você precisa adicionar novos testes ao sistema e remover testes desnecessários dele. A regeneração de instantâneos é uma tarefa simples, mas qual é o valor desses testes? Mesmo que eles consigam encontrar um erro, seria melhor que o perdessem em uma série de alterações de snapshots e simplesmente verificassem novos snapshots sem gastar muito tempo com isso.
Como resultado, esses testes não ajudam particularmente a refatoração. Idealmente, nenhum teste deve falhar se eu executar a refatoração, após o que o usuário vê e com o que ele interage não mudou. E vice-versa - se eu alterasse o contato do usuário, pelo menos um teste falharia. Se os testes seguirem essas duas regras, elas serão uma excelente ferramenta para garantir que algo encontrado pelos usuários não seja alterado durante a refatoração.
▍ Princípio nº 4. Os testes devem reproduzir como usuários reais trabalham com o aplicativo
Eu gostaria que os testes falhassem apenas se algo mudasse com o qual o usuário interage. Isso significa que os testes devem funcionar com o aplicativo da mesma maneira que os usuários trabalham com ele. Por exemplo, um teste deve interagir verdadeiramente com os elementos do formulário e, assim como um usuário, deve inserir texto nos campos de entrada de texto. Os testes não devem acessar componentes e chamar métodos de seu ciclo de vida de forma independente, não devem escrever algo no estado dos componentes ou fazer algo que se baseie nas complexidades da implementação de componentes. Como, finalmente, quero verificar a parte do sistema que está em contato com o usuário, é lógico garantir que os testes, ao interagir com o sistema, reproduzam as ações de usuários reais o mais próximo possível.
Ferramentas de teste
Agora que definimos os objetivos que queremos alcançar, vamos falar sobre quais ferramentas escolhemos para isso.
▍TypeScript
Nossa base de código usa o TypeScript. Nossos serviços de back-end são escritos em Ir e interagem entre si usando o gRPC. Isso nos permite gerar clientes gRPC digitados para uso em um servidor GraphQL. Os resolvedores do servidor GraphQL são digitados usando os tipos gerados usando o
graphql-code-generator . E, finalmente, nossas consultas, mutações, bem como componentes de assinatura e ganchos são totalmente digitados. A cobertura total de nossa base de código com tipos elimina toda uma classe de erros causados pelo fato de o formulário de dados não ser o que o programador espera. A geração de tipos dos arquivos de esquema e protobuf garante que todo o nosso sistema, em todas as partes da pilha de tecnologias utilizadas, permaneça homogêneo.
EstJest (teste de unidade)
Como uma estrutura para testar código, usamos
Jest e
@ testing-library / react . Nos testes criados com essas ferramentas, testamos funções ou componentes isoladamente do restante do sistema. Normalmente, testamos funções e componentes que são usados com mais freqüência em um aplicativo ou aqueles que têm várias maneiras de executar código. É difícil verificar esses caminhos durante a integração ou o teste de ponta a ponta (E2E).
Os testes de unidade para nós são um meio de testar peças pequenas. Os testes de integração e de ponta a ponta fazem um excelente trabalho de verificação do sistema em uma escala maior, permitindo verificar o nível geral de integridade do aplicativo. Mas, às vezes, você precisa garantir que os pequenos detalhes estejam funcionando e escrever testes de integração para todos os usos possíveis do código é muito caro.
Por exemplo, precisamos verificar se a navegação do teclado funciona no componente responsável por trabalhar com a lista suspensa. Mas, ao mesmo tempo, não queremos verificar todas as variantes possíveis desse comportamento ao testar o aplicativo inteiro. Como resultado, testamos minuciosamente a navegação isoladamente e, ao testar páginas usando o componente apropriado, prestamos atenção apenas na verificação de interações de nível superior.
Ferramentas de teste
PressCypress (testes de integração)
Os testes de integração criados usando o
Cypress são o núcleo do nosso sistema de testes. Quando começamos a criar o portal StackPath, esses foram os primeiros testes que escrevemos, pois são altamente valiosos, com muito pouco custo adicional para sua criação. O Cypress exibe todo o aplicativo em um navegador e executa scripts de teste. Todo o front-end funciona exatamente da mesma maneira que quando os usuários trabalham com ele. É verdade que a camada de rede do sistema é substituída por mokami. Cada consulta de rede que normalmente chegaria ao servidor GraphQL retorna dados condicionais para o aplicativo.
O uso de zombarias para simular a camada de rede de um aplicativo tem muitos pontos fortes:
- Os testes são mais rápidos. Mesmo que o back-end do projeto seja extremamente rápido, o tempo necessário para retornar respostas às solicitações feitas durante todo o conjunto de testes pode ser bastante substancial. E se Moki for responsável pelo retorno das respostas, as respostas serão retornadas instantaneamente.
- Os testes estão se tornando mais confiáveis. Uma das dificuldades de executar testes completos de um projeto é que é necessário levar em consideração o estado variável dos dados da rede e do servidor, que podem sofrer alterações. Se o acesso real à rede for simulado usando moxas, essa variabilidade desaparecerá.
- É fácil reproduzir situações que exigem a repetição exata de determinadas condições. Por exemplo, em um sistema real, será difícil fazer com que certas solicitações falhem de maneira estável. Se você precisar verificar a reação correta do aplicativo a solicitações sem êxito, o moki permitirá que você reproduza facilmente situações de emergência.
Embora a substituição de todo o back-end por mok pareça uma tarefa assustadora, todos os dados condicionais são digitados usando os mesmos tipos de TypeScript gerados usados no aplicativo. Isto é - esses dados, pelo menos - em termos de estrutura, são garantidos como equivalentes aos retornados por um back-end normal. Durante a maioria dos testes, suportamos pacificamente as desvantagens de usar mooks em vez de chamadas reais de servidor.
Além disso, os programadores têm muito prazer em trabalhar com o Cypress. Os testes são executados no Cypress Test Runner. As descrições de teste são exibidas à esquerda e o aplicativo de teste é executado no elemento
iframe
principal. Após iniciar o teste, você pode estudar seus estágios individuais e descobrir como o aplicativo se comportou uma vez ou outra. Como a ferramenta para executar testes é executada no próprio navegador, você pode usar as ferramentas do navegador do desenvolvedor para depurar os testes.
Ao escrever testes de front-end, geralmente leva muito tempo para comparar o que o teste faz com o estado do DOM em um determinado ponto do teste. O Cypress simplifica bastante essa tarefa, pois o desenvolvedor pode ver tudo o que acontece com o aplicativo em teste.
Aqui está um videoclipe que demonstra isso.
Esses testes ilustram perfeitamente nossos princípios de teste. A proporção de seu valor e seu "preço" nos convém. Os testes reproduzem de maneira muito semelhante as ações do usuário real que interage com o aplicativo. E apenas a camada de rede do projeto foi substituída por mokami.
PressCypress (teste de ponta a ponta)
Nossos testes E2E também são escritos usando Cypress, mas neles não usamos moki para simular o nível de rede de um projeto ou para simular qualquer outra coisa. Ao executar testes, o aplicativo acessa o servidor GraphQL real, que funciona com instâncias reais de serviços de back-end.
O teste de ponta a ponta é extremamente valioso para nós. O fato é que são os resultados desses testes que nos permitem saber se algo funciona conforme o esperado ou não. Nenhuma simulação é usada durante esse teste; como resultado, o aplicativo funciona exatamente da mesma maneira que quando é usado por clientes reais. No entanto, deve-se notar que os testes de ponta a ponta são "mais caros" do que outros. Eles são mais lentos, mais difíceis de escrever, dada a possibilidade de falhas de curto prazo durante sua implementação. É necessário mais trabalho para garantir que o sistema permaneça em um estado conhecido antes de executar os testes.
Os testes geralmente precisam ser executados no momento em que o sistema está em algum estado conhecido. Após a conclusão do teste, o sistema muda para outro estado conhecido. No caso de testes de integração, não é difícil atingir esse comportamento do sistema, pois as chamadas para a API são substituídas por mokas e, como resultado, cada teste é executado em condições predeterminadas controladas pelo programador. Porém, no caso dos testes E2E, já é mais difícil fazer isso, pois o data warehouse do servidor contém informações que podem mudar durante o teste. Como resultado, o desenvolvedor precisa encontrar uma maneira de garantir que, quando o teste for iniciado, o sistema esteja em um estado conhecido anteriormente.
No início da execução de teste de ponta a ponta, executamos um script que, fazendo chamadas diretas para a API, cria uma nova conta com pilhas, sites, cargas de trabalho, monitores e similares. Cada sessão de teste implica o uso de uma nova instância dessa conta, mas todo o resto, de tempos em tempos, permanece inalterado. O script, tendo feito tudo o que é necessário, forma um arquivo que contém os dados usados para executar os testes (geralmente contém informações sobre identificadores de instância e domínios). Como resultado, o script permite que você coloque o sistema em um estado conhecido antes de executar os testes.
Como o teste de ponta a ponta é "mais caro" do que outros tipos de teste, nós, em comparação com os testes de integração, escrevemos menos testes de ponta a ponta. Nós nos esforçamos para garantir que os testes abranjam os recursos críticos do aplicativo. Por exemplo, isso é registrar usuários e seu logon, criar e configurar um site / carga de trabalho e assim por diante. Graças a extensos testes de integração, sabemos que, em geral, nosso front-end é funcional. Porém, testes de ponta a ponta são necessários apenas para garantir que, ao conectar o front-end ao back-end, algo não aconteça que outros testes não possam detectar.
Contras de nossa abrangente estratégia de testes
Embora estejamos muito satisfeitos com os testes e a estabilidade do aplicativo, também há desvantagens em usar uma estratégia abrangente de testes como a nossa.
Para começar, a aplicação de tal estratégia de teste significa que todos os membros da equipe devem estar familiarizados com muitas ferramentas de teste, e não apenas com uma. Todo mundo precisa conhecer Jest, @ testing-library / react e Cypress. Mas, ao mesmo tempo, os desenvolvedores não precisam apenas conhecer essas ferramentas. Eles também precisam ser capazes de tomar decisões sobre em qual situação qual deve ser usada. Vale a pena testar alguma nova oportunidade de escrever um teste de ponta a ponta ou o teste de integração é suficiente? É necessário, além do teste de ponta a ponta ou de integração, escrever um teste de unidade para verificar os pequenos detalhes da implementação desse novo recurso?
Sem dúvida, isso, por assim dizer, "sobrecarrega a cabeça" de nossos programadores, enquanto usa a única ferramenta que eles não experimentariam tal carga. Geralmente começamos com testes de integração e, depois disso, se percebermos que o recurso em estudo é de particular importância e depende fortemente da parte do servidor do projeto, adicionamos o teste de ponta a ponta apropriado. Ou começamos com testes de unidade, fazendo isso se acreditarmos que um teste de unidade não será capaz de verificar todas as sutilezas da implementação de um determinado mecanismo.
Obviamente, ainda estamos diante de situações em que não está claro por onde começar. Mas, como constantemente precisamos tomar decisões em relação a testes, certos padrões de situações comuns começam a surgir. Por exemplo, geralmente testamos sistemas de validação de formulários usando testes de unidade. Isso é feito devido ao fato de que durante o teste você precisa verificar muitos cenários diferentes. Ao mesmo tempo, todos na equipe sabem disso e não perdem tempo planejando uma estratégia de teste quando um deles precisa testar o sistema de validação de formulário.
Outra desvantagem da abordagem que usamos é a complicação de coletar dados sobre a cobertura de código por testes. Embora isso seja possível, é muito mais complicado do que em uma situação em que alguém é usado para testar um projeto. Embora a busca de um número bonito de cobertura de código por testes possa levar a uma deterioração na qualidade dos testes, essas informações são valiosas em termos de encontrar “brechas” no conjunto de testes usado. O problema do uso de várias ferramentas de teste é que, para entender qual parte do código não foi testada, você precisa combinar relatórios sobre a cobertura do código com os testes recebidos de diferentes sistemas. É possível, mas é definitivamente muito mais difícil do que ler um relatório gerado por qualquer meio de teste.
Sumário
Ao usar muitas ferramentas de teste, fomos confrontados com tarefas difíceis. Mas cada uma dessas ferramentas serve a seu próprio propósito. No final, acreditamos que fizemos a coisa certa, incluindo-os em nosso sistema de teste de código. Testes de integração - é aqui que é melhor começar a criar um sistema de teste no início do trabalho em um novo aplicativo ou ao equipar os testes de um projeto existente. Será útil tentar adicionar testes de ponta a ponta ao projeto o mais cedo possível, verificando os recursos mais importantes do projeto.
Quando o conjunto de testes contém testes de ponta a ponta e de integração, isso deve levar ao fato de que o desenvolvedor receberá um certo nível de confiança na integridade do aplicativo quando forem feitas alterações nele. Se, durante o curso do trabalho no projeto, começaram a aparecer erros que não são detectados pelos testes, vale a pena considerar quais testes poderiam detectar esses erros e se a aparência dos erros indica as falhas de todo o sistema de teste usado no projeto.
Obviamente, não chegamos imediatamente ao nosso sistema de testes atual. Além disso, esperamos que esse sistema, à medida que nosso projeto cresça, se desenvolva. Mas agora realmente gostamos da nossa abordagem aos testes.
Caros leitores! Quais estratégias você segue nos testes de front-end? Quais ferramentas de teste de front-end você usa?
