Muitos desenvolvedores não gostam de testar, mas é um aspecto importante da engenharia de software que afeta diretamente a qualidade do código. Testes instáveis ​​não ajudam a detectar bugs ao escrever o código, o que anula todo o propósito do teste.

Além disso, os testes funcionam como uma parte da documentação para outros desenvolvedores. Ao ler os testes que você criou, eles devem ter uma boa compreensão da finalidade do código que você desenvolveu.

Este artigo aborda nove práticas recomendadas para teste de JavaScript que podem ajudá-lo a escrever testes melhores e ajudar sua equipe a entender melhor os testes que você criou. Vamos nos concentrar em três elementos específicos:

  1. Teste a anatomia e as descrições do teste
  2. Antipadrões de teste de unidade
  3. Preparação para o teste

Vamos começar!

1. Anatomia do teste e descrições do teste

Esta seção explora como você pode melhorar a anatomia e as descrições de seus testes. O objetivo é melhorar a legibilidade dos seus arquivos de teste para que os desenvolvedores possam examiná-los rapidamente para encontrar as informações que desejam.

Por exemplo, eles atualizaram uma função e querem entender quais testes exigem alterações. Você pode realmente ajudá-los aplicando estrutura aos seus testes e escrevendo descrições de teste cuidadosas.

1.1-Testes de estrutura com o padrão AAA

No início, o padrão AAA pode não lhe dizer nada-então vamos esclarecer! O padrão AAA significa A rrange, A ct e A ssert. Você deseja dividir a lógica dentro dos testes em três partes para torná-los mais fáceis de entender.

A parte “organizar” inclui todo o código de configuração e dados de teste de que você precisa para simular um cenário de teste. Em segundo lugar, como o próprio nome indica, a parte “agir” executa o teste de unidade. Normalmente, a execução do teste consiste apenas em uma ou duas linhas de código. E, por último, a parte “assert” agrupa todas as asserções onde você compara a saída recebida com a saída esperada.

Aqui está um exemplo que demonstra isso:

 it ('deve resolver com"true"quando o bloco é forjado pelo delegado correto', async ()=> { //Arranjo bloco const={ altura: 302, carimbo de data/hora: 23450, generatorPublicKey:'6fb2e0882cd9d895e1e441b9f9be7f98e877aa0a16ae230ee5caceb7a1b896ae', }; //Agir resultado const=espera dpos.verifyBlockForger (bloco); //Assert esperar (resultado).toBeTrue ();
}); 

Se você comparar a estrutura de teste acima com o exemplo abaixo, ficará claro qual é mais legível. Você terá que gastar mais tempo lendo o teste abaixo para descobrir o que ele faz, enquanto a abordagem acima torna visualmente claro como o teste é estruturado.

 it ('deve resolver com"true"quando o bloco é forjado pelo delegado correto', async ()=> { bloco const={ altura: 302, carimbo de data/hora: 23450, generatorPublicKey:'6fb2e0882cd9d895e1e441b9f9be7f98e877aa0a16ae230ee5caceb7a1b896ae', }; resultado const=espera dpos.verifyBlockForger (bloco); esperar (resultado).toBeTrue ();
}); 

1.2-Escreva descrições de teste detalhadas usando o sistema de 3 camadas

Pode parecer fácil escrever descrições de teste detalhadas, mas há um sistema que você pode aplicar para tornar as descrições de teste ainda mais simples de entender. Sugiro estruturar testes usando um sistema de três camadas:

  • Camada 1: unidade que você deseja testar ou requisito de teste
  • Camada 2: ação ou cenário específico que você deseja testar
  • Camada 3: Descreva o resultado esperado

Aqui está um exemplo deste sistema de três camadas para escrever descrições de teste. Neste exemplo, vamos testar um serviço que lida com pedidos.

Aqui, queremos verificar se a funcionalidade para adicionar novos itens ao carrinho de compras funciona conforme o esperado. Portanto, escrevemos dois casos de teste da “Camada 3”, onde descrevemos o resultado desejado. É um sistema fácil que melhora a digitalização de seus testes.

 describe ('OrderServcie', ()=> { descrever ('Adicionar um novo item', ()=> { it ('Quando o item já está na cesta de compras, espera-se que a contagem de itens aumente', async ()=> { //... }); it ('Quando o item não existe no carrinho de compras, espera-se que a contagem do item seja igual a um', async ()=> { //... }); });
}); 

2. Antipadrões de teste de unidade

Os testes de unidade são cruciais para validar sua lógica de negócios-eles se destinam a detectar erros lógicos em seu código. É a forma mais rudimentar de teste porque você deseja que sua lógica esteja correta antes de começar a testar componentes ou aplicativos por meio do teste E2E.

2.1-Evite testar métodos privados

Já vi muitos desenvolvedores testando os detalhes de implementação de métodos privados. Por que você os testaria se pode cobri-los testando apenas métodos públicos? Você terá falsos positivos se os detalhes de implementação que realmente não importam para o seu método exposto mudarem, e você terá que gastar mais tempo mantendo testes para métodos privados.

Aqui está um exemplo que ilustra isso. Uma função privada ou interna retorna um objeto, e você também verifica o formato desse objeto. Se agora você alterar o objeto retornado para a função privada, seu teste falhará mesmo que a implementação esteja correta. Não há nenhum requisito para permitir que os usuários calculem o IVA, apenas mostre o preço final. No entanto, nós falsamente insistimos aqui para testar as partes internas da classe.

 class ProductService { //Método interno-mude o nome da chave do objeto e o teste abaixo irá falhar CalculeVATAdd (priceWithoutVAT) { return {finalPrice: priceWithoutVAT * 1.2}; } //método público getPrice (productId) { const neededProduct=DB.getProduct (productId); finalPrice=this.calculateVATAdd (desejadoProduct.price).finalPrice; return finalPrice; }
} it ('Quando os métodos internos obtêm 0 IVA, ele retorna 0 resposta', async ()=> { expect (new ProductService (). calculVATAdd (0).finalPrice).to.equal (0);
}); 

2.2-Evite detectar erros nos testes

Costumo ver desenvolvedores que usam uma instrução try... catch para detectar erros em testes e usá-los em asserções. Essa não é uma boa abordagem porque deixa a porta aberta para falsos positivos.

Se você cometer um erro na lógica da função que está tentando testar, é possível que a função não gere um erro quando você espera que ela gere um. Portanto, o teste ignora o bloco catch e passa-mesmo que a lógica de negócios esteja incorreta.

Aqui está um exemplo que espera que a função addNewProduct gere um erro quando você cria um novo produto sem fornecer um nome de produto. Se a função addNewProduct não gerar um erro, seu teste será aprovado porque há apenas uma asserção fora do bloco try... catch que verifica o número de vezes que a função foi chamado.

 it ('Quando não há preço do produto, gera erro', async ()=> { deixe expectError=null; tentar { const result=await addNewProduct ({name:'rollerblades'}); } catch (erro) { expect (error.msg).to.equal ("Nenhum nome de produto"); errorWeExceptFor=erro; } expect (errorWeExceptFor).toHaveBeenCalledTimes (1)
}); 

Então, como você pode reescrever este teste? Jest, por exemplo, oferece aos desenvolvedores uma função toThrow onde você espera que a invocação da função gere um erro. Se a função não gerar um erro, a declaração falhará.

 it ('Quando não há preço do produto, gera erro', async ()=> { aguarde, espere (addNewProduct ({name:'rollerblades'})) .toThrow (AppError) .with.property ("msg","Nenhum nome de produto");
}); 

2.3-Não zombe de tudo

Alguns desenvolvedores simulam todas as chamadas de função em testes de unidade, então eles acabam testando instruções if... else . Esses testes são inúteis porque você pode confiar em uma linguagem de programação para implementar uma instrução if... else corretamente.

Você só deve simular as dependências subjacentes ou de nível mais baixo e as operações de E/S, como chamadas de banco de dados, chamadas de API ou chamadas para outros serviços. Dessa forma, você pode testar os detalhes de implementação de métodos privados.

Por exemplo, o exemplo abaixo ilustra uma função getPrice que chama um método interno calculVATAdd , que por si só chama uma API com getVATPercentage . Não zombe da função calcularVATAdd ; queremos verificar os detalhes de implementação desta função.

Assim, devemos simular apenas a chamada de API externa getVATPercentage porque não temos nenhum controle sobre os resultados retornados por esta API.

 class ProductService { //Método interno CalculeVATAdd (priceWithoutVAT) { const vatPercentage=getVATPercentage ();//chamada de API externa-> Mock const finalprice=priceWithoutVAT * vatPercentage; return finalprice; } //método público getPrice (productId) { const neededProduct=DB.getProduct (productId); finalPrice=this.calculateVATAdd (desejadoProduct.price);//Não zombe deste método, queremos verificar os detalhes de implementação return finalPrice; }
} 

2.4-Use dados realistas

Nem todo desenvolvedor gosta de criar dados de teste. Mas os dados de teste devem ser tão realistas quanto possível para cobrir tantos caminhos de aplicativo quanto possível para detectar defeitos. Portanto, existem muitas estratégias de geração de dados para transformar e mascarar os dados de produção para usá-los em seus testes. Outra estratégia é desenvolver funções que gerem entrada aleatória.

Resumindo, não use a string de entrada típica foo para testar seu código.

//Classe Faker para gerar dados aleatórios específicos do produto
const name=faker.commerce.productName ();
const product=faker.commerce.product ();
número const=faker.random.number ()); 

2.5-Evite muitas afirmações por caso de teste

Não tenha medo de dividir os cenários ou escrever descrições de teste mais específicas. Um caso de teste que contém mais de cinco afirmações é um sinalizador vermelho potencial; indica que você está tentando verificar muitas coisas de uma vez.

Em outras palavras, a descrição do seu teste não é específica o suficiente. Além disso, ao escrever casos de teste mais específicos, torna-se mais fácil para os desenvolvedores identificar os testes que exigem mudanças ao fazer atualizações de código.

Dica : use uma biblioteca como faker.js para ajudá-lo a gerar dados de teste realistas.

3. Preparação para o teste

Esta última seção descreve as práticas recomendadas para a preparação do teste.

3.1-Evite muitas bibliotecas auxiliares

Freqüentemente, é bom abstrair muitos requisitos de configuração complexos usando bibliotecas auxiliares. Muita abstração pode se tornar muito confusa, no entanto, especialmente para desenvolvedores que são novos em seu conjunto de testes.

Você pode ter um caso extremo em que precisa de uma configuração diferente para completar um cenário de teste. Agora se torna muito difícil e confuso criar sua configuração de caso extremo. Além disso, abstrair muitos detalhes pode confundir os desenvolvedores, porque eles não sabem o que está acontecendo nos bastidores.

Como regra geral, você deseja que os testes sejam fáceis e divertidos. Suponha que você precise gastar mais de 15 minutos para descobrir o que está acontecendo nos bastidores durante a configuração em um gancho beforeEach ou beforeAll . Nesse caso, você está complicando demais sua configuração de teste. Isso pode indicar que você está copiando muitas dependências. Ou o oposto: nada de stub, criando uma configuração de teste muito complexa. Esteja ciente disso!

Dica : você pode medir isso fazendo com que um novo desenvolvedor descubra seu conjunto de testes. Se demorar mais de 15 minutos, isso indica que sua configuração de teste pode ser muito complexa. Lembre-se, o teste deve ser fácil!

3.2-Não abuse dos ganchos de preparação de teste

Apresentando muitos ganchos de preparação de teste- beforeAll , beforeEach , afterAll , afterEach , etc.-while aninhá-los em blocos describe torna-se uma verdadeira bagunça para entender e depurar. Aqui está um exemplo da documentação do Jest que ilustra a complexidade:

 beforeAll (()=> console.log ('1-beforeAll'));
afterAll (()=> console.log ('1-afterAll'));
beforeEach (()=> console.log ('1-beforeEach'));
afterEach (()=> console.log ('1-afterEach')); test ('', ()=> console.log ('1-teste')); describe ('Escopo/bloco aninhado', ()=> { beforeAll (()=> console.log ('2-beforeAll')); afterAll (()=> console.log ('2-afterAll')); beforeEach (()=> console.log ('2-beforeEach')); afterEach (()=> console.log ('2-afterEach')); teste ('', ()=> console.log ('2-teste'));
}); //1-beforeAll
//1-beforeEach
//1-teste
//1-afterEach
//2-beforeAll
//1-beforeEach
//2-beforeEach
//2-teste
//2-afterEach
//1-afterEach
//2-afterAll
//1-depois de tudo 

Esteja atento ao usar ganchos de preparação de teste. Use ganchos apenas quando desejar introduzir comportamento para todos os seus casos de teste. Mais comumente, os ganchos são usados ​​para ativar ou desativar processos para executar cenários de teste.

Conclusão

O teste pode parecer simples no início, mas há muitas coisas que você pode melhorar para torná-lo mais divertido para você e seus colegas. Seu objetivo é manter seus testes fáceis de ler, examinar e manter. Evite configurações complexas ou muitas camadas de abstração, o que aumenta a complexidade do teste.

Você pode impactar significativamente a qualidade e legibilidade de seus testes, introduzindo o sistema de três camadas e o padrão AAA. É um pequeno esforço que retorna muito valor para sua equipe. Não se esqueça de considerar as outras práticas recomendadas descritas nesta postagem do blog.

A postagem Teste de JavaScript: 9 práticas recomendadas para aprender apareceu primeiro em LogRocket Blog .

Source link