Análise de mutação ou como testar testes

Nunca há muitos testes - todo mundo sabe disso. Memes sobre testes de unidade e integração não são mais muito divertidos. Mas ainda não sabemos se é possível confiar nos resultados da aprovação nos testes e qual porcentagem de cobertura nos permitirá não permitir que erros sejam produzidos. Se alterações fatais no código ignoram os testes, sem afetar o resultado, a solução sugere a si mesma - você precisa testar os testes!



A abordagem para automatizar essa tarefa foi o relatório de Mark Langovoy no Frontend Conf . O vídeo e o artigo são curtos, e as idéias são muito úteis - você precisa tomar nota.


Sobre o palestrante: Mark Langovoi ( marklangovoi ) trabalha em Yandex no projeto Yandex.Tolok . Esta é uma plataforma de crowdsourcing para marcar rapidamente grandes quantidades de dados. Os clientes baixam dados que, por exemplo, precisam ser preparados para uso em algoritmos de aprendizado de máquina, e estabelecem um preço e, por outro lado, os artistas podem concluir tarefas e ganhar dinheiro.

Em seu tempo livre, Mark desenvolve o Krasnodar Devodar Developer Days, uma das 19 comunidades de TI cujos ativistas convidamos para o Frontend Conf em Moscou.

Teste


Existem diferentes tipos de testes automatizados.


Durante testes de unidade populares , escrevemos testes para pequenas peças (módulos) de um aplicativo. Eles são fáceis de escrever, mas às vezes durante a integração com outros módulos, eles podem não se comportar exatamente como esperávamos.

Para evitar isso, podemos escrever testes de integração que testarão a operação de nossos módulos juntos.


Eles são um pouco mais complicados, então hoje vamos nos concentrar nos testes de unidade.

Teste de unidade


Qualquer projeto que queira pelo menos alguma estabilidade mínima está escrevendo testes de unidade.

Considere um exemplo.

class Signal { on(callback) { ... } off(callback) { const callbackIndex = this.listeners.indexOf(callback); if (callbackIndex === -1) { return; } this.listeners = [ ...this.listeners.slice(0, callbackIndex - 1), ...this.listeners.slice(callbackIndex) ]; } trigger() { ... } } 

Existe uma classe Signal - este é um emissor de eventos, que possui um método ativado para inscrição e um método desativado para excluir uma assinatura - verificamos se o retorno de chamada está contido na matriz de assinantes e, em seguida, excluí-lo. E, é claro, existe um método de acionador que chamará retornos de chamada assinados.

Temos um teste simples para este exemplo que chama os métodos on e off e, em seguida, o acionador, para verificar se o retorno de chamada não foi chamado após o cancelamento da inscrição.

 test('off method should remove listener', () => { const signal = new Signal(); let wasCalled = false; const callback = () => { wasCalled = true; }; signal.on(callback); signal.off(callback); signal.trigger(); expect(wasCalled).toBeFalsy(); }); 

Critérios de avaliação da qualidade


Quais são os critérios para avaliar a qualidade desse teste?

A cobertura de código é o critério mais popular e conhecido que mostra quantos por cento das linhas de código foram executadas quando o teste foi executado.


Você pode ter uma cobertura de código de 70%, 80% ou toda a 90%, mas isso significa que quando você coletar a próxima compilação para produção, tudo ficará bem ou algo pode dar errado?

Vamos voltar ao nosso exemplo.

Sexta à noite, você está cansado, está terminando o próximo recurso. E então você encontra esse código que seu colega escreveu. Algo sobre você parecia complicado e assustador para você.

  ...this.listeners.slice(0, callbackIndex - 1), ...this.listeners.slice(callbackIndex) 

Você decidiu que provavelmente pode limpar a matriz:

 class Signal { ... off(callback) { const callbackIndex = this.listeners.indexOf(callback); if (callbackIndex === -1) { return; } this.listeners = []; } ... } 

Comprometi-me, montei o projeto e enviei-o para produção. Testes aprovados - por que não? E ele foi descansar em um bar.



Mas de repente, tarde da noite, uma ligação tocou no receptor, dizendo que tudo estava caindo, as pessoas não podiam usar o produto e, em geral, os negócios desperdiçavam dinheiro! Você queima, você está ameaçado de demissão.



Como lidar com isso? O que fazer com os testes? Como pegar tais erros estúpidos primitivos? Quem testará os testes?

Obviamente, você pode contratar um exército de engenheiros de controle de qualidade - deixe-os sentar e aplaudir nosso aplicativo.



Ou contrate a automação do controle de qualidade. Eles podem ser responsabilizados por escrever testes - por que escrever por conta própria se há pessoas especiais para isso?

Mas, na verdade, é caro, então hoje falaremos sobre análise mutacional ou teste mutacional.

Teste de mutação


Essa é uma maneira de automatizar o processo de teste de nossos testes. Seu objetivo é identificar testes ineficazes e incompletos, isto é, de fato, são testes de teste .

A idéia é alterar trechos de código, executar testes neles e, se os testes não caírem, eles estarão incompletos.

As alterações são feitas usando certas operações - mutadores . Eles substituem, por exemplo, mais por menos, multiplicam por divisão e outras operações semelhantes. Os mutadores podem alterar trechos de código, substituir condições em enquanto, zero matrizes em vez de adicionar algum elemento à matriz.


Como resultado da aplicação de mutações ao código-fonte, ele sofre mutação e se torna um mutante .

Os mutantes são divididos em duas categorias:

  1. Morto - aqueles em que conseguimos identificar desvios, ou seja, nos quais pelo menos um teste caiu.
  2. Os sobreviventes foram os que fugiram de nós e trouxeram o bug para a produção.

Para avaliar a qualidade, existe uma métrica MSI (Mutation Score Indicator) - a porcentagem de mutantes mortos e sobreviventes. Quanto maior a diferença entre testes de cobertura de código e MSI, pior a porcentagem de cobertura de código reflete a relevância de nossos testes.

Isso era um pouco de teoria, e agora considere como ele pode ser usado em JavaScript.

Solução Javascript


Existe apenas uma ferramenta de teste de mutação em desenvolvimento ativo no JavaScript - Stryker . Este nome foi dado ao instrumento em homenagem ao personagem do X-homem William Stryker - o criador de "Arma X" e um lutador com todos os mutantes.



Stryker não é um corredor de teste como Karma ou Jest; nem é uma estrutura de teste como Mocha ou Jasmine. Essa é uma estrutura de teste de mutação que complementa sua infraestrutura atual.

Sistema de plug-in


O Stryker é muito flexível, totalmente construído em um sistema de plug-in, a maioria dos quais é escrita por desenvolvedores do Stryker.


Existem plugins para executar testes no Jest, Karma e Mocha. Existe integração com os frameworks Mocha (stryker-mocha-framework) Jasmine (stryker-jasmine) e conjuntos de mutadores prontos para JavaScript, TypeScript e até para o Vue:

  • mutador stryker-javascript;
  • stryker-typescript;
  • Stryker-Vue-Mutator.

Mutators for React estão incluídos no stryker-javascript-mutator. Além disso, você sempre pode escrever seus próprios mutadores.

Se você precisar converter o código antes da execução, poderá usar plug-ins para Webpack, Babel ou TypeScript.


Tudo isso é configurado de forma relativamente simples.

Configuração


A configuração não é difícil: você só precisa especificar na configuração JSON qual executor de teste (e / ou estrutura de teste e / ou transpiler) você usa, além de instalar os plugins apropriados a partir do npm.

O simples utilitário de console stryker-cli pode fazer tudo isso em um modo de pergunta e resposta. Ela perguntará o que você usa e se configurará.

Como isso funciona?


O ciclo de vida é simples e consiste nas seguintes etapas:

  • Lendo e analisando a configuração. Stryker baixa a configuração e analisa-a para vários plugins, configurações, exclusão de arquivos, etc.
  • Faça o download de plugins de acordo com a configuração.
  • Executando testes no código-fonte para verificar se os testes são relevantes agora (de repente eles já estão quebrados).
  • Se tudo estiver bem, um conjunto de mutantes será gerado a partir dos arquivos que permitimos que sejam alterados.
  • Executando testes em mutantes.



Acima está um exemplo de como iniciar o Stryker:

  • Stryker inicia;
  • lê uma configuração;
  • carrega as dependências necessárias;
  • localiza arquivos que sofrerão mutação;
  • executa testes no código fonte;
  • cria 152 mutantes;
  • executa testes em 8 threads (neste caso, com base no número de núcleos da CPU).

Como não é um processo rápido, é melhor fazê-lo em alguns servidores de CI / CD.

Após passar em todos os testes, Stryker fornece um breve relatório sobre os arquivos com o número de mutantes criados, mortos e sobreviventes, bem como a porcentagem da proporção de mutantes mortos para sobreviventes (MSI) e os mutadores usados.

Esses são problemas em potencial que não estavam previstos em nossos testes.

Resumir


O teste de mutação é útil e interessante . Ele pode encontrar problemas nos estágios iniciais dos testes e sem a participação de pessoas. Isso reduzirá o tempo para validação de solicitação de recebimento, por exemplo, devido ao fato de que desenvolvedores qualificados não terão que gastar tempo na validação de solicitação de recebimento, que já tem problemas em potencial. Ou salve a produção se você decidir preparar um novo lançamento na sexta à noite.

Stryker é uma ferramenta flexível de teste de mutações multithread. Está em desenvolvimento ativo, mas até agora úmido, ainda não chegou à versão principal. Por exemplo, durante a preparação deste relatório, seus desenvolvedores finalmente tornaram possível no plug-in para Babel especificar o arquivo de configuração e corrigir a integração Jest. Este é um projeto OpenSource que pode ser ajudado a desenvolver.

Perguntas frequentes
- Como testar testes de mutação? Certamente, também há um erro. No primeiro exemplo de teste de unidade, a cobertura foi de 90%. Parece que está tudo bem, mas ainda assim os casos escaparam quando tudo caiu e pegou fogo. Por conseguinte, por que deveria haver a sensação de que está tudo bem depois de cobrir esses testes mutacionais?

- Não estou dizendo que o teste de mutação é uma bala de prata e curará tudo. Naturalmente, pode haver alguns casos loucos limítrofes ou a ausência de algum tipo de mutador. Primeiro de tudo, erros típicos são facilmente capturados. Por exemplo, você coloca uma verificação na idade, define-a para <18 (era necessário <=) e no teste esqueceu de fazer uma verificação de caso de fronteira. Você fez outra comparação com o mutador e, como resultado, o teste caiu (ou não caiu) e você entende que tudo está bom ou tudo está ruim. Tais coisas são rapidamente capturadas. Esta é uma maneira de simplesmente anexar testes corretamente, encontrar pontos ausentes.

- Muitas vezes você tem uma situação de "atordoado e saiu"? Eu acho que isso não é verdade.

- Não, mas acho que em muitos projetos essas coisas existem. Naturalmente, isso não é verdade. Muitas pessoas pensam que a cobertura do código ajuda a verificar tudo, você pode sair com segurança e não se preocupar - mas não é assim.

- Direi imediatamente qual é o problema. Temos muitos tipos de redutores e outras coisas que testamos mutacionalmente, e existem muitos deles. Isso tudo cresce e acontece que, para cada solicitação pull, o teste de mutação é iniciado, o que leva muito tempo. É possível executar apenas o que mudou?

"Eu acho que você pode configurá-lo você mesmo." Por exemplo, no lado do desenvolvedor, quando ele pressiona, confirma, você pode criar um plug - in com fiapos que executará apenas arquivos que foram alterados. No CI / CD, isso também é possível. No nosso caso, o projeto é muito grande e antigo, e praticamos testes no local. Nós não verificamos tudo, porque vai demorar uma semana, haverá centenas de milhares de mutações. Eu recomendaria fazer verificações no local ou organizar eu mesmo um processo seletivo de inicialização. Eu não vi uma ferramenta pronta para essa integração.

- A integridade de todas as possíveis mutações é fornecida para um pedaço de código específico? Se não, como exatamente as mutações são selecionadas?

- Eu pessoalmente não verifiquei, mas também não encontrei nenhum problema com isso. Stryker deve gerar todas as mutações possíveis no mesmo trecho de código.

- Quero perguntar sobre instantâneos. Meu teste de unidade testa a lógica e, incluindo o layout do componente de reação de captura instantânea. Naturalmente, se eu mudar algum design lógico, meu layout mudará ali. Esse é o comportamento esperado, não é?

- Sim, esse é o significado deles: você atualiza os instantâneos manualmente.

- Então, de alguma forma, você ignora os instantâneos neste relatório?

- Provavelmente, os instantâneos precisam ser atualizados com antecedência e, em seguida, executar o teste de mutação; caso contrário, haverá muito lixo do Stryker.

- Pergunta sobre servidores de CI. Para testes de unidade simples, no GitLab existem repórteres para o que você quiser, que exibe a porcentagem de testes bem-sucedidos que você pode configurar, com ou sem falha. E o Stryker? Ele apenas exibe o tablet no console, mas o que posso fazer a seguir?

- Eles têm repórter em HTML, você pode criar seus próprios repórteres - tudo é personalizável de forma flexível. Talvez haja algumas ferramentas específicas, mas como ainda estamos testando a mutação pontual, não encontrei integrações específicas com o TeamCity e ferramentas similares de CI / CD.

- Quanto os testes de mutação aumentam o suporte para os testes que você tem em geral? Ou seja, os testes são difíceis e os testes precisam ser reescritos quando o código é reescrito, etc. Às vezes, é mais fácil reescrever o código do que os testes. E aqui também tenho testes mutacionais. Quanto custa para uma empresa?

- Primeiro, provavelmente corrigirei se o código de reescrita para fins de teste está errado. O código deve ser fácil de testar. Quanto ao que precisa ser concluído, é novamente importante para os negócios que os testes sejam o mais completos e eficazes possíveis. Se eles não estiverem completos, isso significa que pode ocorrer um erro que trará perdas. Naturalmente, você pode testar apenas as partes mais importantes para os negócios.

"Ainda assim, quanto mais caro se torna quando os testes de mutação aparecem, do que se eles não estivessem lá".

- São tantos testes ruins agora. Se os testes forem mal escritos agora, você terá que adicionar muito. O teste de mutação encontrará casos que não são cobertos por testes.

- No slide com os resultados da verificação Stryker, existem muitos vorings, eles são críticos ou não críticos. Como lidar com falsos positivos?

- A questão sutil é o que é considerado falso. Perguntei aos caras da nossa equipe o que aconteceu de interessante com eles. Houve um exemplo sobre o texto do erro. Stryker relatou que os testes não responderam à alteração do texto do erro. Parece ser um batente, mas menor.

- Você vê esses erros e ignora os não críticos no modo manual?

"Temos uma verificação no local, então sim."

Eu tenho uma pergunta prática. Quando você implementou isso, qual porcentagem dos testes você falhou?

- Não o implementamos em todo o projeto, mas houve pequenos problemas no novo projeto. Portanto, não sei dizer os números exatos, mas, em geral, a abordagem definitivamente melhorou a situação.

Veja outras apresentações front-end, igualmente úteis, em nosso canal no youtube , todos os relatórios temáticos de todas as nossas conferências chegam gradualmente lá. Ou assine a newsletter e manteremos você informado sobre todos os novos materiais e notícias de futuras conferências.

Source: https://habr.com/ru/post/pt421141/


All Articles