Introdução aos testes

Publicados: 2021-06-16

Bem-vindo à nossa nova série de blogs sobre tudo relacionado a testes . Esperamos que essas postagens do blog lhe dêem uma ideia geral de como as escrevemos aqui no Mediatoolkit e por quê. Antes de mergulharmos nos exemplos, vamos começar com algumas definições básicas, ideias e benefícios de escrever testes.

Que tipo de testes existem?

Nem todos os testes são criados iguais. Existem diferentes tipos de testes para diferentes propósitos. Embora este post se concentre principalmente em testes de unidade , devemos estar cientes das diferenças e entender os benefícios de cada um.

1. Testes manuais

O teste manual envolve testes executados manualmente por um testador. Geralmente são feitos pelo Quality Assurance ou pelos próprios desenvolvedores . O QA deve ser visto como a última linha de defesa, não o principal testador de aplicativos (o QA fazer testes manuais como a principal maneira de testar a interface do usuário é uma exceção válida para esse caso).

Os desenvolvedores geralmente escrevem testes manuais quando querem executar algumas coisas localmente e ver como o sistema se comporta sem empurrar nenhum código para a fase de teste, ou Deus salve você, produção. No entanto, o objetivo desses testes não é a robustez do código. O único papel desses testes é detectar bugs e garantir a qualidade do seu produto .

2. Testes de integração

Os sistemas são compostos de múltiplos componentes, ou pelo menos deveriam ser. Os testes de integração verificam a API pública desses componentes. Não apenas a API REST, mas qualquer API exposta publicamente, como a comunicação do tópico Kafka.

Se um componente “diz” que envia uma mensagem sobre um tópico em um determinado formato, essa é sua API pública ou contrato. O teste de integração está verificando se vários componentes desenvolvidos individualmente podem se comunicar e se seus contratos correspondem como um grupo.

Se você testar esses componentes apenas individualmente, seu comportamento individual pode funcionar corretamente, mas quando você tenta conectá-los em um grupo maior, descobre que seus contratos são diferentes .

3. Testes de ponta a ponta (E2E)

O teste de ponta a ponta garante o fluxo de experiência do usuário do produto final . A complexidade dos sistemas hoje é difícil de cobrir com testes. Muitos sistemas dependem uns dos outros e os testes E2E garantem que o produto faça o que é esperado . O QA valida a exatidão do seu produto passando pelo fluxo de usuários finais e verificando se todos os sistemas se comportam conforme o esperado.

4. Testes unitários

Os testes unitários são a espinha dorsal de qualquer software confiável . Eles fazem a base para outros testes. Eles testam unidades individuais.

Os desenvolvedores podem confundir a definição de uma unidade com um único método. Os testes de unidade devem testar o comportamento de um componente que pode ser composto de várias classes diferentes. Métodos públicos acessíveis por outros componentes precisam ser testados, não teste métodos ou classes protegidas. Eles são detalhes de implementação, não fazem parte de sua API pública .

Por que devemos mesmo testar?

Se escrevermos bons testes e os escrevermos com frequência, podemos garantir a qualidade do nosso produto antes que ele veja a luz do dia.

À medida que nosso sistema envelhece, os benefícios dos testes se tornam cada vez mais aparentes. Nossos ambientes se tornam confiáveis, economizam tempo de desenvolvimento e muito esforço quando inevitavelmente as coisas dão errado. Seus colegas ficarão agradecidos quando puderem dar uma olhada em seus testes e entender o que seu código deve e não deve fazer sem ter que executar as coisas manualmente .

Código robusto

Antes de começarmos a falar sobre “código robusto”, como devemos defini-lo? O que torna o código robusto? Isso significa que devemos programar defensivamente e pensar em como outros desenvolvedores podem abusar do nosso código?

O código robusto é sempre simples e limpo. A simplicidade é robusta, a complexidade é frágil . Devemos lidar com entradas inválidas, mas isso não significa que precisamos programar defensivamente e não confiar em nossa equipe.

Lei de Gall : Um sistema complexo que funciona invariavelmente evoluiu de um sistema simples que funcionou.

A proposição inversa também parece ser verdadeira: um sistema complexo projetado do zero nunca funciona e não pode ser feito para funcionar. Você tem que começar de novo, começando com um sistema simples e funcional.

Refatorar com segurança

Quando seu código é coberto por testes, você não tem mais medo de alterar o código existente . Após cada alteração, você pode executar seus testes e certificar-se de que não quebrou nada. Quando você tem testes, não precisa programar defensivamente.

Refatorar sem testes é descer uma ladeira escorregadia que terminará em noites sem dormir e domingos de trabalho. Este tópico é muito amplo para ser abordado aqui e merece um post próprio no futuro.

Como não escrevemos testes?

É tão importante saber como NÃO escrever testes de unidade , quanto como escrevê-los.

  • Escrever um teste que imprima o resultado de uma chamada de método não é um teste, pois não validamos o resultado desejado .
  • Se seu teste lê dados de um arquivo em sua pasta Documentos, não é um teste de unidade real, pois os testes não devem depender do ambiente.
  • Qualquer desenvolvedor deve ser capaz de verificar seu código e executar os testes com sucesso sem fazer mais nada.
  • Cada teste de unidade deve ser independente de outros testes . Isso implica que a ordem de execução de seus testes também não deve importar.
  • A execução de testes várias vezes deve sempre terminar com os mesmos resultados se não alterarmos nenhum código.
  • Os testes de unidade devem testar comportamentos , não chamadas de métodos individuais. Nem toda classe e método precisa ter seu teste.

Comportamento é algo que produz um valor real que o usuário do seu sistema precisa. Seu usuário precisa saber se productFactory.create() criou o mesmo objeto quando chamado duas vezes ou se seu repositório foi chamado com alguns parâmetros? Provavelmente não, mas muitos desenvolvedores ainda escrevem exatamente esses tipos de testes.

Se seus testes se parecem com isso, eles estão fortemente acoplados à sua implementação. Cada vez que você deseja alterar os detalhes de sua implementação, você precisa atualizar seus testes, mesmo que o comportamento seja o mesmo. Seus testes devem mudar apenas quando o comportamento mudar, não os detalhes de implementação . Em outras palavras, teste o que seu código faz, não como ele faz.

Como escrevemos testes?

Nossos testes devem seguir as melhores práticas de código , devem ser independentes do ambiente e precisam ser executados rapidamente .

É importante manter o tempo de execução do teste o mais curto possível. Cada teste não deve demorar mais do que alguns milissegundos . Quando os testes demoram muito para serem executados, as pessoas tendem a ignorá-los e apenas confiar em seu servidor de CI, como o Jenkins, para fazer barulho quando não puder criar seus executáveis ​​de implantação.

Cada teste é composto por 3 seções 'A' (O padrão AAA) :

  1. Arranjo
  2. ato
  3. Afirmar

1. Organizar

Na seção de organização de nosso teste, garantimos que nosso sistema esteja em um estado específico antes de chamar o comportamento que queremos testar . O 'sistema' pode ser um objeto que precisamos configurar de uma maneira específica para produzir comportamento, criando arquivos temporários ou coisas dessa natureza.

O código nesta seção é geralmente maior que os outros dois combinados .

Um padrão de projeto que deve ser particularmente útil para manter esta seção pequena é Object Mother . Esse padrão de design é muito semelhante ao Factory , mas possui métodos mais específicos que constroem objetos pré-configurados para você. Enquanto uma Factory padrão pode ter um método como createCar(carDescription) , uma ObjectMother terá métodos como createRedFerrari() , createBlackTesla() ou createBrokenYugo() .

2. Agir

Esta seção do seu teste deve ter uma linha . Esta linha executa o comportamento em teste. Se você estiver escrevendo mais de uma linha para esta seção, provavelmente não tem o encapsulamento correto de seu comportamento . Não se espera que seus clientes chamem vários métodos de seu objeto em uma ordem específica, então por que seus testes?

Esta linha é uma chamada de método que queremos testar. Se esse método retornar um resultado, você deve armazenar esse valor em uma variável para verificar se é o valor esperado na etapa Assert.

3. Afirmar

Depois de prepararmos o sistema na seção Organizar e executar nossa ação em teste na seção Agir, precisamos validar o resultado da ação. Geralmente verificamos o resultado do método aqui, mas às vezes nossos métodos não retornam valores, mas ainda produzem efeitos colaterais. Se era esperado que nosso código alterasse o estado de um objeto, criasse um arquivo ou removesse algo de um List , devemos verificar se ele fez exatamente isso.

Stubs vs Mocks

A maioria dos desenvolvedores usa os termos mock e stub de forma intercambiável, mas há diferenças.

Um stub não pode falhar no teste, um mock pode.

Os stubs são baseados em estado , eles retornam valores codificados. “Que resultado eu obtive?”

Os mocks são baseados em comportamento , você os usa para verificar como seu comportamento passa por ele. “Como cheguei ao resultado?”

Se seus testes de unidade estiverem cheios de mocks, seus testes acabarão muito frágeis, o que significa que cada vez que você alterar um de seus detalhes de implementação, você precisará atualizar todas as suas chamadas de mock.

Teste apenas uma coisa.

Precisamos ser capazes de isolar um comportamento e provar que funciona . Se esse comportamento funcionar de maneira diferente com entradas diferentes, precisamos escrever um novo teste para cada um desses comportamentos. É difícil saber por que nosso teste falhou se tivermos um teste grande que testa várias coisas ao mesmo tempo. Além disso, fica mais difícil remover recursos que não precisamos mais e ver quais recursos quebramos quando adicionamos um novo código.

Não há problema em ter várias declarações na seção final de seus testes, desde que isso não exija que você chame o comportamento em teste várias vezes. Se for esse o caso, será difícil identificar o comportamento defeituoso e corrigi-lo.

Quando afirmamos vários comportamentos em um teste, não obtemos uma imagem clara do que exatamente não funciona porque o teste relatará apenas a primeira falha e o restante será ignorado. Fica muito mais difícil entender quais mudanças são necessárias e quantas coisas não funcionam como esperado.

Testes de nomenclatura

Uma coisa que separa bons testes de ótimos testes é o nome do teste. Os testes não devem apenas nos dizer o que fazem, mas quando o fazem.

Existem muitos padrões de nomenclatura bons que você pode usar, então escolha o que achar mais descritivo e fique com ele. Aqui estão alguns exemplos de ótimos nomes de teste:

  • RegistrationServiceShould.createNewAccountWhenEmailIsNotTaken
  • RegistrationServiceTest.whenEmailIsFree_createNewAccount
  • RegistrationServiceTest.if_freeEmail_when_userCreatesAccount_then_create

Quando você escreve testes de unidade, é muito mais importante comunicar o que você está testando do que seguir as melhores práticas de nomenclatura de métodos. Por exemplo, em Java, usamos camelCase ao escrever métodos, mas é perfeitamente válido usar sublinhado (_) para separar o estado da ação em seu nome de teste.

Testes limpos

Os testes que você escreve devem seguir todas as práticas de código limpo que você aplica ao seu código. Os testes não são cidadãos de segunda classe e você precisa aplicar o mesmo nível de cuidado que faz com o restante do código para torná-los legíveis.

A definição de duplicação de código em testes é muito importante. O princípio DRY (Don't Repeat Yourself) se aplica à extração de comportamento que muda pelo mesmo motivo. Os testes mudam por motivos diferentes, portanto, seja variado ao extrair coisas de seus testes se eles realmente não mudarem pelo mesmo motivo. Alerta de spoiler, muitas vezes não.

if as instruções não pertencem aos testes. A instrução if nos diz que nosso teste faz pelo menos duas coisas diferentes e seria melhor reescrevermos nosso teste como dois testes diferentes. Quando nomeado corretamente, será mais fácil entender o que os testes fazem e quais são todos os diferentes comportamentos.

Quando devemos escrever testes?

Guiados pelos princípios do TDD, devemos escrever testes antes de escrever um novo código .

Quando precisamos adicionar um novo recurso, primeiro descrevemos o comportamento desejado como um novo teste. Fazemos a menor quantidade de alterações necessárias para passar nesse teste sem quebrar nenhum outro.

Caso contrário, quanto mais o tempo passa, mais código temos que não foi testado e a chance de introduzir bugs ou overengineering aumenta .

Além disso, os testes se tornam mais complexos, pois temos mais código para testar agora, e o que geralmente acontece é que os desenvolvedores ajustam os testes ao código. Isso significa que ajustamos o comportamento para corresponder ao que nosso código faz, e não o contrário.

Quando escrevemos testes cedo, a definição do problema se torna menor e é mais fácil entender tais questões do que comportamentos mais genéricos e complexos.

Pensamentos finais

Embora escrever testes possa parecer uma coisa opcional a fazer, é crucial começar com boas bases. A codificação já é difícil, torne mais fácil para você e seus colegas de equipe escrevendo código testável que seja mais fácil de ler, entender e manter . Finalmente, se você tiver dificuldades para fazer seus testes cumprirem seus desejos, o problema é mais provável em seu código do que em seus testes.

Interessado em trabalhar em um código limpo e testável?
Confira nossa vaga aberta para Desenvolvedor Backend Sênior
ou envie-nos uma candidatura aberta !