Pessoas geralmente começam engatinhando, depois andam, evoluem para corrida, e algumas fazem coisas mais estranhas, como 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.
Como conheci "testes de testes"
Muito prazer, eu sou o Bux! 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? 🤔
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 (que por baixo dos panos utiliza a coverage.py). Com alguns parâmetros, podemos saber quais linhas de um arquivo específico estão "descobertas" por um teste específico.
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
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), 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):
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 é:
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! 👋
Top comments (0)