Métodos e classes de teste

Concluído

Além de escrever funções de teste, o Pytest permite que você use classes. Como já mencionamos, não há necessidade de herança e as classes de teste seguem algumas regras simples. O uso de classes oferece mais flexibilidade e reutilização. Como você verá a seguir, o Pytest não atrapalha e evita obrigar você a escrever testes de uma maneira específica.

Assim como funções, você também pode escrever declarações usando a instrução assert.

Criar uma classe de teste

Vamos usar um cenário do mundo real para ver como as classes de teste podem ajudar. A função a seguir verifica se um determinado arquivo contém "sim" em seu conteúdo. Em caso afirmativo, ele retornará True. Se o arquivo não existir ou se ele contiver "não" em seu conteúdo, ele retornará False. Esse cenário é comum em tarefas assíncronas que usam o sistema de arquivos para indicar a conclusão.

Veja a aparência da função:

import os

def is_done(path):
    if not os.path.exists(path):
        return False
    with open(path) as _f:
        contents = _f.read()
    if "yes" in contents.lower():
        return True
    elif "no" in contents.lower():
        return False

Agora, veja como fica uma classe com dois testes (um para cada condição) em um arquivo chamado test_files.py:

class TestIsDone:

    def test_yes(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("yes")
        assert is_done("/tmp/test_file") is True

    def test_no(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("no")
        assert is_done("/tmp/test_file") is False

Cuidado

Os métodos de teste estão usando o caminho /tmp para um arquivo de teste temporário porque é mais fácil de usar para o exemplo. No entanto, se você precisa usar arquivos temporários, considere usar uma biblioteca como tempfile que pode criá-los (e removê-los) com segurança para você. Nem todo sistema tem um diretório /tmp, e esse local pode não ser temporário dependendo do sistema operacional.

Executar os testes com o sinalizador -v para aumentar o detalhamento mostra os testes aprovados:

pytest -v test_files.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 
cachedir: .pytest_cache
rootdir: /private/
collected 2 items

test_files.py::TestIsDone::test_yes PASSED                               [ 50%]
test_files.py::TestIsDone::test_no PASSED                                [100%]

============================== 2 passed in 0.00s ===============================

Embora os testes sejam aprovados, eles parecem repetitivos e também estão deixando os arquivos após a conclusão do teste. Antes de vermos como aprimorá-los, abordaremos os métodos auxiliares na próxima seção.

Métodos auxiliares

Em uma classe de teste, você pode usar alguns métodos para configurar e desinstalar a execução do teste. O Pytest os executará automaticamente se forem definidos. Para usar esses métodos, você precisa saber que eles têm uma ordem e um comportamento específicos.

  • setup: é executado uma vez antes de cada teste em uma classe
  • teardown: é executado uma vez após cada teste em uma classe
  • setup_class: é executado uma vez antes de todos os testes em uma classe
  • teardown_class: é executado uma vez após todos os testes em uma classe

Quando os testes exigem recursos semelhantes (ou idênticos) para funcionar, é útil escrever métodos de instalação. O ideal é que um teste não deixe recursos para trás após sua conclusão, portanto, métodos de desinstalação podem ajudar na limpeza do teste nessas situações.

Limpeza

Veja uma classe de teste atualizada que limpa os arquivos após cada teste:

class TestIsDone:

    def teardown(self):
        if os.path.exists("/tmp/test_file"):
            os.remove("/tmp/test_file")

    def test_yes(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("yes")
        assert is_done("/tmp/test_file") is True

    def test_no(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("no")
        assert is_done("/tmp/test_file") is False

Como usamos o método teardown(), essa classe de teste não deixa mais um /tmp/test_file para trás.

Instalação

Outra melhoria que podemos fazer a essa classe é adicionar uma variável que aponta para o arquivo. Como agora o arquivo é declarado em seis locais, alterações no caminho significariam alterá-lo em todos esses pontos. Este exemplo mostra a aparência da classe com um método setup() adicionado que declara a variável de caminho:

class TestIsDone:

    def setup(self):
        self.tmp_file = "/tmp/test_file"

    def teardown(self):
        if os.path.exists(self.tmp_file):
            os.remove(self.tmp_file)

    def test_yes(self):
        with open(self.tmp_file, "w") as _f:
            _f.write("yes")
        assert is_done(self.tmp_file) is True

    def test_no(self):
        with open(self.tmp_file, "w") as _f:
            _f.write("no")
        assert is_done(self.tmp_file) is False

Métodos auxiliares personalizados

Você pode criar métodos auxiliares personalizados em uma classe. Esses métodos não podem ser prefixados com o nome test nem ser nomeados como os métodos de instalação ou de limpeza. Na classe TestIsDone, podemos automatizar a criação do arquivo temporário em um auxiliar personalizado. Esse método auxiliar personalizado pode ser semelhante a este exemplo:

    def write_tmp_file(self, content):
        with open(self.tmp_file, "w") as _f:
            _f.write(content)

O Pytest não executa automaticamente o método write_tmp_file(), e outros métodos podem chamá-lo diretamente para economizar em tarefas repetitivas, como gravar em um arquivo.

A classe inteira se parece com este exemplo após a atualização dos métodos de teste para usar o auxiliar personalizado:

class TestIsDone:

    def setup(self):
        self.tmp_file = "/tmp/test_file"

    def teardown(self):
        if os.path.exists(self.tmp_file):
            os.remove(self.tmp_file)

    def write_tmp_file(self, content):
        with open(self.tmp_file, "w") as _f:
            _f.write(content)

    def test_yes(self):
        self.write_tmp_file("yes")
        assert is_done(self.tmp_file) is True

    def test_no(self):
        self.write_tmp_file("no")
        assert is_done(self.tmp_file) is False

Quando usar uma classe em vez de uma função

Não há regras rígidas com relação a quando usar uma classe em vez de uma função. É sempre uma boa ideia seguir as convenções em projetos e equipes atuais com as quais você está trabalhando. Veja algumas perguntas gerais a serem feitas que podem lhe ajudar a determinar quando usar uma classe:

  • Seus testes precisam de um código auxiliar de limpeza ou configuração semelhante?
  • Faz sentido lógico agrupar seus testes?
  • Há pelo menos alguns testes em seu conjunto de testes?
  • Seus testes podem se beneficiar de um conjunto comum de funções auxiliares?