Testando testes no Python - Parte 1: motivos e alternativas

vbuxbaum

Vitor Buxbaum Orlandi

Posted on September 27, 2023

Testando testes no Python - Parte 1: motivos e alternativas

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? 🤔

Batman pensativo

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.

Exemplo de uso do  raw `pytest-cov` endraw

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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! 👋

💖 💪 🙅 🚩
vbuxbaum
Vitor Buxbaum Orlandi

Posted on September 27, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related