Testando testes no Python - Parte 1: motivos e alternativas
Tags: python, testing, braziliandevs
Published: 2023-09-27 13:30:32

> Pessoas geralmente começam engatinhando, depois andam, evoluem para corrida, e algumas fazem coisas mais estranhas, como [Parkour](https://www.google.com/search?q=parkour).

> Pessoas Devs geralmente começam codando, depois testam, evoluem para o TDD, e algumas fazem coisas mais estranhas, como **testes de testes**. 

---

Boas vindas! 🤩 Esse é o primeiro artigo de uma curta série, contando um pouco mais sobre "testes de testes". 🧪

Vou discutir as motivações, alternativas, e detalhar as formas que fazemos em projetos de Python na [Trybe](https://www.betrybe.com/).

## Como conheci "testes de testes"

Muito prazer, eu sou o [Bux](https://www.linkedin.com/in/vitorbuxbaum/)! No momento em que escrevo esse texto, sou Especialista em Instrução (i.e., professor e produtor de conteúdo) na Trybe, uma escola de tecnologia brasileira, onde trabalho há quase 3 anos.

No nosso curso de Desenvolvimento Web, estudantes realizam **projetos** em que avaliamos se aprenderam o conteúdo. Nesses projetos temos testes automatizados, e a pessoa estudante deve implementar o código necessário para passar nos testes para conseguir a aprovação.

Por exemplo: ao ensinarmos Flask podemos ter um projeto que exige a implementação de um CRUD de músicas, e criamos testes que validam os requisitos desse CRUD. Se a implementação da pessoa estudante passar em nossos testes, ela está aprovada. ✅

Mas o que acontece quando ensinamos a pessoa a criar testes automatizados? **Como vamos avaliar se ela criou testes adequados?** 

## O que são os testes de testes

Em resumo, "teste de testes" é o código que fazemos para responder a última pergunta, ou seja: foram criados testes de software que atendem aos requisitos informados?

Essa é uma pergunta que pode ser reflexão de qualquer time de pessoas desenvolvedoras (_ou analistas de qualidade_) profundamente preocupadas com a qualidade do teste desenvolvido. Mas, no caso do nosso time, a intenção era "apenas" avaliar se a turma consegue criar bons testes de software.

Imagine que, por exemplo, a pessoa estudante precise criar testes para uma função que busca por livros no banco de dados a partir de uma string em seu título. Essa busca pode ter mais detalhes, como ser case-insensitive, retornar conteúdo paginado, etc. Eu preciso ter uma forma (automatizada) de garantir que a pessoa criou testes para essa função.

## O que _você_ faria?

Antes de avançar na leitura, pare para refletir um pouco: como você faria isso? 🤔

![Batman pensativo](https://media.giphy.com/media/a5viI92PAF89q/giphy.gif)

## Algumas alternativas

### Ferramenta de cobertura de testes

Essa é uma das formas mais simples para validar se um trecho de código está sendo testado. A maioria das linguagens modernas possuem formas de averiguar, quando rodamos um teste, quantas e quais linhas do código fonte estão sendo testadas.

No Python podemos usar o plugin do Pytest chamado [pytest-cov](https://pytest-cov.readthedocs.io/en/latest/) (que por baixo dos panos utiliza a [coverage.py](https://coverage.readthedocs.io/)). Com alguns parâmetros, podemos saber quais linhas de um arquivo específico estão "descobertas" por um teste específico. 

![Exemplo de uso do `pytest-cov`](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mufrvzxzcrxhu5ortgy3.png)

Eu posso então executar os testes da pessoa estudante, e aprová-la caso tenha 100% de cobertura! 🎉

> 🟢 Vantagens: é simples de construir e dar manutenção, e podemos aplicar a praticamente qualquer contexto. Além disso, a ferramenta tem um feedback direto e explícito (essencial num contexto de educação)

> 🔴 Desvantagens: cobertura de testes não é uma métrica de qualidade de testes "infalível", já que podemos conseguir 100% de cobertura sem fazer um único `assert`.

### Ferramenta de testes de mutação

🌟 Testes de mutação funcionam a partir de uma ideia simples mas brilhante:

> Os testes de uma unidade devem passar com a implementação correta dessa unidade, e **devem falhar com implementações incorretas dessa unidade**

Bibliotecas como o [`mutmut`](https://mutmut.readthedocs.io/en/latest/) podem fazer isso por nós: a ferramenta gera mutações ("sujeiras") no código fonte e roda os testes novamente. As mutações são realizadas a nível da [AST (árvore sintática abstrata)](https://en.wikipedia.org/wiki/Abstract_syntax_tree), como trocar comparadores (`<` por `>=`) ou booleanos (`True` por `False`). 

Um bom teste é aquele que **_falha_** para todas as mutações. Se o seu teste continua passando para alguma das mutações, significa que ele pode melhorar validando mais casos de uso.

> 🟢 Vantagens: podemos ter mais confiança de que **bons testes** estão sendo feitos pela pessoa estudante, e não apenas que a função/unidade está sendo executada.

> 🔴 Desvantagens: ferramentas de mutação adicionam complexidade na execução do teste, o que pode impactar a experiência de aprendizado (lembre-se, queremos utilizar em projetos didáticos). Além disso, as mutações oferecidas por essas ferramentas são limitadas, genéricas e não possuem nenhuma 'estratégia', o que pode levar a uma lentidão no processo.

### Testes de mutações customizadas

A ideia por trás da alternativa anterior é, como já disse, brilhante! 🌟

A mutação customizada (um conceito que provavelmente estou criando agora, com ajuda do ChatGPT) utiliza essa mesma ideia mas com uma implementação diferente: ao invés de gerarmos automaticamente diversas mutações aleatórias em um arquivo, escolhemos exatamente as mutações que desejamos com uso de dublês.

Imagine o seguinte cenário: pedimos para que estudantes criem testes para a seguinte classe `Queue` (_Queue = Fila, uma estrutura de dados sequencial em que inserimos elementos no fim, e removemos do início_):

```python
class Queue():
    def __init__(self):
        self.__data = []  

    def __len__(self):
        return len(self.__data)

    def enqueue(self, value):
        self.__data.append(value)

    def dequeue(self):
        try:
            return self.__data.pop(0)
        except IndexError:
            raise LookupError("A fila está vazia")
```

O que faremos, então, é criar versões "quebradas" da classe `Queue` (com mutações estratégicas) e rodar os testes novamente. Se os testes estiverem bem escritos, eles devem **_falhar_** com as mutações. 

Uma possível classe com mutação para esse caso é:

```python
from src.queue import Queue

# Repare que estou usando herança para reaproveitar
# os métodos da classe original e realizar mutação 
# apenas no método 'dequeue'
class WrongExceptionQueue(Queue):
    def dequeue(self):
        try:
            return self.__data.pop(0)
        except IndexError:
            raise ValueError("A fila está vazia")
```

Ou seja: se a pessoa estudante não fez um teste que valida o _tipo da exceção_ levantada pelo método `dequeue`, ela não será aprovada.

> 🟢 Vantagens: continuamos tendo confiança de que **bons testes** estão sendo feitos pela pessoa estudante, e não apenas que a função/unidade está sendo executada. Mas além disso, temos mais liberdade para criar mutações complexas e estratégicas para cada requisito, e não precisamos executar os testes com mutações desnecessárias (que não agregam ao aprendizado) poupando tempo e recursos computacionais. E como bônus, podemos dar feedbacks (essenciais no processo de aprendizado) extremamente customizados como, por exemplo: "Você lembrou de validar qual exceção é levantada no método `dequeue`?"

> 🔴 Desvantagens: é necessário esforço para pensar e codificar as mutações, já que não serão geradas automaticamente. E, além disso, **não existe (_ou pelo menos não conseguimos encontrar_) uma biblioteca que facilite a configuração desse tipo de teste**.

## Se não existe, a gente cria! 🚀

E foi isso que fizemos: criamos o código necessário para aplicar as mutações customizadas nos testes. 🤓

No próximo artigo dessa série vou detalhar como funciona 1ª versão (já estamos caminhando para a 3ª) dos nossos "testes de testes" com mutações customizadas. Até lá, queria saber: como **você** implementaria essa funcionalidade?

Nos ~~vemos~~ lemos em breve! 👋