Classes e métodos de teste

Concluído

Além de escrever funções de teste, 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. A utilização de aulas dá-lhe mais flexibilidade e reutilização. Como você vê a seguir, Pytest se mantém fora do caminho e evita forçá-lo a escrever testes de uma determinada maneira.

Assim como as funções, você ainda pode escrever asserções usando a assert instrução.

Criar uma classe de teste

Vamos usar um cenário do mundo real para ver como as aulas de teste podem ajudar. A função a seguir verifica se um determinado arquivo contém "sim" em seu conteúdo. Se sim, ele retorna True. Se o arquivo não existir ou se 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 como fica a 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

Atenção

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ê precisar usar arquivos temporários, considere usar uma biblioteca como tempfile essa 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 para aumentar a -v verbosidade 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 estejam passando, eles parecem repetitivos e também estão deixando arquivos depois que o teste é feito. Antes de vermos como podemos melhorá-los, vamos abordar os métodos auxiliares na próxima seção.

Métodos auxiliares

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

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

Quando os testes exigem recursos semelhantes (ou idênticos) para funcionar, é útil escrever métodos de configuração. Idealmente, um teste não deve deixar recursos quando for concluído, então os métodos de demolição podem ajudar na limpeza do teste nessas situações.

Limpeza

Vamos examinar 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 teardown() método, essa classe de teste não deixa mais um /tmp/test_file para trás.

Configuração

Outra melhoria que podemos fazer nesta classe é adicionar uma variável que aponta para o arquivo. Como o arquivo agora é declarado em seis lugares, qualquer alteração no caminho significaria alterá-lo em todos esses pontos. Este exemplo mostra como a classe se parece com um método adicionado setup() que declara a variável path:

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 devem ser prefixados com o nome test e não podem ser nomeados como os métodos de configuração ou limpeza. TestIsDone Na classe, poderíamos automatizar a criação do arquivo temporário em um auxiliar personalizado. Esse método auxiliar personalizado pode se parecer com 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 write_tmp_file() método, e outros métodos podem chamá-lo diretamente para salvar em tarefas repetitivas, como gravar em um arquivo.

A classe inteira se parece com este exemplo, depois de atualizar os 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 sobre quando usar uma classe em vez de uma função. É sempre uma boa ideia seguir as convenções em projetos atuais e equipes com as quais você está trabalhando. Aqui estão algumas perguntas gerais a serem feitas que podem ajudá-lo a determinar quando usar uma classe:

  • Seus testes precisam de configuração semelhante ou código auxiliar de limpeza?
  • Agrupar os testes faz sentido lógico?
  • Existem pelo menos alguns testes no seu conjunto de testes?
  • Seus testes poderiam se beneficiar de um conjunto comum de funções auxiliares?