PARTE 1 - Tudo que um desenvolvedor precisa saber sobre testes
Tags: testing, architecture
Published: 2023-05-18 03:32:53

## A importância dos testes

Realizar testes é fundamental para garantir que um software esteja funcionando corretamente, mas essa tarefa pode ser desafiadora, já que é impossível testar todas as combinações de entrada e saída. É importante, portanto, escolher quais casos serão testados e variar os tipos de entrada para encontrar bugs e entender a fronteira do sistema. É comum que bugs não estejam distribuídos de forma uniforme, sendo importante testar componentes que apresentam mais falhas com mais frequência. Vale lembrar que testes não são perfeitos e não garantem que um software não tenha bugs. Por isso, muitos times também investem em qualidade de código e monitoramento. A finalidade dos testes é verificar se o software funciona de acordo com os requisitos estabelecidos, ou seja, a verificação é crucial para garantir o funcionamento correto do sistema.

## Criar bons casos de teste

O teste de software é um processo importante para garantir que um programa esteja funcionando de acordo com o esperado. Para isso, é fundamental que os casos de teste sejam capazes de identificar possíveis bugs no sistema. Entretanto, encontrar falhas pode ser um desafio, especialmente quando se trata de entender os limites do programa e identificar casos excepcionais.

Uma abordagem sistemática nos testes é essencial para identificar casos críticos e garantir que nenhum detalhe seja deixado de lado. Existem diversas técnicas para testar de forma sistemática, e uma delas é a técnica de "Testes baseados em especificação". Essa técnica consiste em partir dos requisitos do programa e pensar nele como uma caixa fechada, sem saber exatamente como ele funciona. O objetivo é dar uma entrada e verificar a saída.

Para utilizar essa técnica, o primeiro passo é analisar cada entrada separadamente e identificar o que pode ser testado em cada uma delas. É importante observar como essas entradas interagem entre si e verificar se há alguma regra de domínio que não foi considerada na especificação. Além disso, é fundamental entender quais são as saídas esperadas do programa.

Com essas informações em mãos, é possível combinar tudo e derivar casos de teste. No entanto, é importante lembrar que é impossível fazer um teste exaustivo, já que o número de combinações de entrada e saída é infinito. Por isso, é necessário simplificar e focar nos casos excepcionais que podem ser testados apenas uma vez. Dessa forma, é possível garantir que o programa funcione corretamente dentro do esperado.

Em resumo, uma abordagem sistemática nos testes é fundamental para identificar casos críticos e garantir que o programa funcione corretamente. A técnica de "Testes baseados em especificação" é uma das técnicas utilizadas para testar de forma sistemática, e consiste em partir dos requisitos do programa e verificar se ele está funcionando de acordo com o esperado. Com essas informações, é possível simplificar e focar nos casos excepcionais que podem ser testados apenas uma vez para garantir que o programa esteja funcionando corretamente.

## A importância da cobertura de código como ferramenta para melhorar a qualidade dos testes

A cobertura de código não deve ser vista como um objetivo a ser alcançado, mas sim como uma ferramenta para ajudar na melhoria da sua suíte de testes. É importante combinar a cobertura com as técnicas apresentadas acima. Depois de escrever seus testes, questione-se se existem mais casos a serem testados. Verifique sua cobertura para ver se existem linhas de código não testadas e considere se faz sentido criar um teste para esses casos.

Quando você testou seu código baseado nos requisitos, estava dentro de uma caixa fechada e não estava olhando para o seu código-fonte, o que pode fornecer informações úteis para seus testes.

No entanto, é importante lembrar que a cobertura não está sempre relacionada à eficácia dos testes. A cobertura de código é excelente para identificar partes menos testadas, mas não deve ser usada como um alvo de qualidade.

### Cenários em que a cobertura de código pode ser relevante para avaliar a qualidade dos testes

Uma alta cobertura nem sempre significa um bom teste, mas uma baixa cobertura pode indicar que muitas partes do código não estão sendo testadas. É importante lembrar que a cobertura ideal pode variar de acordo com o contexto do sistema. Alguns possíveis cenários são:

- Em relação à cobertura de código, é importante ter em mente que ela é uma ferramenta útil para melhorar a qualidade dos testes, mas não deve ser o objetivo principal. Para alcançar uma cobertura adequada, é fundamental começar criando bons testes e, consequentemente, a cobertura virá como resultado.
- Em relação a sistemas maduros, a cobertura pode ser um indicador relevante para identificar pontos específicos que precisam de mais atenção. Entretanto, ela não deve ser o único fator considerado para avaliar a qualidade dos testes. É importante levar em conta outras métricas e considerar o contexto do sistema.
- A meta de 80% de cobertura é uma referência comum, mas é importante ressaltar que essa porcentagem pode variar de acordo com as necessidades e especificidades do projeto. É crucial lembrar que a cobertura não é uma garantia de qualidade por si só, e que o foco deve estar sempre na criação de testes relevantes e efetivos.

### A relação entre a cobertura de código e a efetividade dos testes

É importante criar testes de qualidade e avaliar outras métricas além da cobertura, levando em conta o contexto do sistema. Embora a cobertura seja útil para identificar áreas menos testadas, uma alta cobertura nem sempre significa que os testes são eficazes, e a cobertura ideal pode variar de acordo com as necessidades e especificidades do projeto. O foco deve estar sempre na criação de testes relevantes e efetivos para garantir a qualidade do software.

O teste de software é um processo importante para garantir que um programa esteja funcionando de acordo com o esperado. Para isso, é fundamental que os casos de teste sejam capazes de identificar possíveis bugs no sistema. Entretanto, encontrar falhas pode ser um desafio, especialmente quando se trata de entender os limites do programa e identificar casos excepcionais.

Uma abordagem sistemática nos testes é essencial para identificar casos críticos e garantir que nenhum detalhe seja deixado de lado. Existem diversas técnicas para testar de forma sistemática, e uma delas é a técnica de "Testes baseados em especificação". Essa técnica consiste em partir dos requisitos do programa e pensar nele como uma caixa fechada, sem saber exatamente como ele funciona. O objetivo é dar uma entrada e verificar a saída.

Para utilizar essa técnica, o primeiro passo é analisar cada entrada separadamente e identificar o que pode ser testado em cada uma delas. É importante observar como essas entradas interagem entre si e verificar se há alguma regra de domínio que não foi considerada na especificação. Além disso, é fundamental entender quais são as saídas esperadas do programa.

Com essas informações em mãos, é possível combinar tudo e derivar casos de teste. No entanto, é importante lembrar que é impossível fazer um teste exaustivo, já que o número de combinações de entrada e saída é infinito. Por isso, é necessário simplificar e focar nos casos excepcionais que podem ser testados apenas uma vez. Dessa forma, é possível garantir que o programa funcione corretamente dentro do esperado.

Em resumo, uma abordagem sistemática nos testes é fundamental para identificar casos críticos e garantir que o programa funcione corretamente. A técnica de "Testes baseados em especificação" é uma das técnicas utilizadas para testar de forma sistemática, e consiste em partir dos requisitos do programa e verificar se ele está funcionando de acordo com o esperado. Com essas informações, é possível simplificar e focar nos casos excepcionais que podem ser testados apenas uma vez para garantir que o programa esteja funcionando corretamente.

### Confiabilidade de Teste - unitário vs integração.

Os testes de unidade são mais fáceis de serem realizados, pois pensar em casos de entrada e fornecer os dados necessários é simples, assim como verificar a saída. Além disso, esses testes são rápidos de executar. No entanto, uma desvantagem dos testes de unidade é que eles não refletem com precisão o mundo real, e certos bugs só podem ser identificados ao testar dois ou mais componentes juntos.

Por outro lado, nos testes de integração, ocorre a interação entre diferentes elementos, tornando-os mais semelhantes ao mundo real. Isso permite identificar bugs que não seriam descobertos em um teste de unidade. No entanto, os testes de integração são mais complexos de serem realizados. Quando testamos vários componentes juntos, é necessário fornecer dados para garantir que os componentes retornem o resultado esperado, o que demanda mais linhas de código. Além disso, pode-se inserir dados nos demais componentes para garantir que eles estejam funcionando corretamente.

Os testes de integração também são mais lentos em sua execução. Portanto, se uma suíte com 100 testes de integração for executada no seu ambiente de integração contínua, é provável que leve mais tempo para concluir e obter feedback sobre possíveis falhas. Além disso, eles também podem demorar para serem executados na máquina do desenvolvedor.

Em conclusão, ambos os tipos de teste são excelentes, mas a questão principal é qual deles fornecerá um feedback de maior qualidade para essa funcionalidade específica.

Se o componente que você deseja testar estiver isolado do sistema, então realizar testes unitários seria a melhor opção. É possível fornecer diversos tipos de entrada facilmente nesse caso.

Agora, imagine que você tenha uma aplicação backend que busca informações no banco de dados, cria uma nova entidade, envia-a novamente para o banco e, por fim, retorna um JSON para o cliente. Para esses casos, não é viável criar um teste de unidade, pois ele não fornecerá um feedback significativo. Se você considerou a possibilidade de simular as chamadas ao banco, saiba que essa abordagem também é equivocada. Se você simular as chamadas, o que exatamente o seu teste estará testando? Basicamente, estará apenas verificando o fluxo da aplicação. Esse tipo de teste ofereceria um feedback limitado. Nessas situações, é muito mais adequado criar um teste de integração que efetivamente acesse o banco de dados, verificando se as consultas estão funcionando corretamente, entre outros aspectos.

Dependendo do contexto, é importante decidir qual tipo de teste é mais apropriado e fornecerá um feedback de qualidade superior.

Os testes de integração não são mágicos, eles não encontram automaticamente bugs. Quem encontra os bugs é o próprio desenvolvedor que está escrevendo o teste, e é o desenvolvedor que identifica os casos de teste. Se você nunca viu um determinado caso de teste, não será capaz de identificar as fronteiras do seu software. Encontrar essas fronteiras pode ser feito tanto com um teste de integração quanto com um teste de unidade - a pessoa é quem irá determiná-las.

Se você possui um componente que interage com diversos outros componentes, é possível testar esses componentes de forma isolada, mas pensando como se estivesse realizando um teste completo de integração. Em vez de executar um teste de integração que demandaria muito esforço, você pode simular o retorno de cada componente, como se fosse o teste completo. É claro que esse tipo de teste pode estar um pouco distante da realidade, mas você ganhará um pouco mais de qualidade para cada um desses testes, pois poderá fornecer diferentes entradas facilmente e também realizar mais testes para os componentes do seu sistema. Geralmente, isso ocorre em sistemas de grande escala, então não fique preso à ideia de realizar testes de integração que envolvam todos os componentes. Divida-os e faça testes separados.

## Importância dos testes simples, rápidos e um bom setup de teste.

Quando escrevemos um teste simples, é muito mais fácil multiplicá-lo e explorar diversas fronteiras do sistema. Isso ocorre porque testes simples são de baixo custo e exigem menos esforço para serem criados. Essa facilidade de multiplicação dos testes leva os desenvolvedores a reconhecer o valor e a importância de escrever mais testes. No entanto, quando os testes se tornam complexos, eles demandam uma grande quantidade de energia, o que pode desmotivar e diminuir a produtividade do desenvolvedor na criação de novos testes. Como resultado, a pessoa pode interromper o processo de escrita de testes e passar para a implementação da próxima funcionalidade do sistema, buscando a sensação de ser produtivo ou útil.

Para facilitar a escrita de testes, é essencial investir no setup de testes do aplicativo. Por exemplo, criar um gerador de mocks pode facilitar a simulação de entradas para um determinado componente. O uso de uma biblioteca para interceptar chamadas HTTP também pode ser útil. Existem diversas outras abordagens que podem ser adotadas para tornar a escrita de testes mais fácil e eficiente.
**Testes simples se multiplicam**

É crucial que os testes sejam rápidos. Se uma suíte de testes demora muito para ser executada, a pessoa terá um efeito semelhante ao dos testes complexos. Ela pode gastar apenas 5 minutos escrevendo um código de produção, mas terá que esperar 3 minutos para a suíte de testes ser concluída. Esse ciclo repetitivo pode afetar a produtividade. Além disso, outra variável a considerar é a integração contínua (CI). Se os testes na CI demoram muito tempo para serem executados, levará mais tempo para identificar problemas. Por isso, se você fizer uma request e o teste for rápido desde o início, você receberá feedback imediato sobre possíveis erros e poderá corrigi-los antes de passar para a próxima tarefa.

Além disso, testes lentos consomem recursos da CI e podem resultar em outros problemas. É importante evitar testes lentos sempre que possível. No entanto, nem sempre isso é viável. Testes lentos geralmente envolvem mais componentes do sistema, o que é inevitável para testes essenciais. É aceitável ter testes lentos, mas eles não devem ser a regra; devem ser a minoria.

## Mocks

Mocks são ferramentas úteis para auxiliar na escrita de testes e simplificar o processo. No entanto, é importante compreender as vantagens e desvantagens do seu uso.

Utilizar mocks envolve um trade-off.

Uma situação adequada para usar mocks é quando eles facilitam a escrita do teste. Por exemplo, se você precisa lidar com um sistema externo complexo, como uma chamada para uma API, e não está preocupado com seu comportamento exato, é possível mocká-lo.

Basicamente, você deve mockar o que é trabalhoso e evitar mockar o que não é.

Outro caso a ser considerado é o uso de mocks em dependências de uma classe. Por exemplo, se você tem uma classe que lida com um carrinho de compras e depende de um repositório, é fácil usar um mock para essa dependência, pois ela não faz parte do contexto geral da classe. No entanto, você não pode usar mocks em dependências internas, como um cálculo dentro da própria classe do carrinho de compras, pois esse cálculo é essencial para o funcionamento da classe e não pode ser mockado.

Se a dependência for externa, talvez você queira simular. Se for interna, talvez você não queira simular.

É importante ter em mente que o mock pode ficar acoplado ao código de produção, o que não é ideal. O teste ideal é aquele em que você importa o mock e não se preocupa com o que ele faz, mas sim com o que ele retorna. Isso oferece a liberdade de alterar o código de produção sem precisar modificar o teste.

Ao mockar um componente, você está criando um contrato entre o componente e o mock. Portanto, se você fizer alguma alteração no componente, precisará atualizar o mock correspondente.

Existem diferentes tipos de mocks:

- Stub: Retorna um valor fixo quando o método é chamado.
- Fake: Implementação real, mas simplificada. Geralmente usado quando a implementação real é muito complexa ou custosa.
- Mock: Além de ter o mesmo comportamento do stub, o mock também pode fazer asserções sobre as interações que ocorrem. Por exemplo, verificar quantas vezes o método foi chamado ou com quais parâmetros ele foi chamado.

É importante entender as peculiaridades e escolher o tipo de mock mais adequado para cada situação de teste.