Acessórios de Pytest
As luminárias fornecem um contexto definido e confiável para testes. recursos personalizados são funções Python decoradas com @pytest.fixture. Use-as para organizar pré-condições, como dados de teste, variáveis de ambiente, conexões de banco de dados ou arquivos de que um teste precisa antes de ser executado. Os recursos também podem definir etapas de desmontagem que liberam os recursos após a conclusão do teste ou do escopo do recurso.
Um teste solicita um recurso declarando um argumento com o mesmo nome do recurso. O Pytest localiza o recurso, o executa e passa o valor retornado ou gerado para o teste. Para obter mais informações sobre o ciclo de vida da fixture, consulte a documentação pytest sobre fixtures.
Os recursos são mais úteis quando a configuração ou a limpeza são reutilizadas, complexas ou ficam mais claras quando nomeadas. Para valores únicos simples, manter o código de instalação diretamente no teste pode ser mais fácil de ler.
As principais características das luminárias pytest incluem:
-
Controle de escopo: os recursos podem usar o parâmetro
scopepara controlar a frequência com que o pytest cria e destrói o valor do recurso. Escopos mais amplos podem melhorar o desempenho, mas também compartilham o estado por mais tempo. -
Inicialização e desmontagem: os recursos podem usar
yieldpara a limpeza. Os recursos avançados também podem receber o recursorequestintegrado (apresentado mais adiante nesta unidade) para inspecionar o teste solicitante e chamarrequest.addfinalizer()para registrar retornos de chamada de limpeza. - Injeção de dependências: os testes e os recursos solicitam os recursos como argumentos, tornando as dependências explícitas.
- Reutilização e modularidade: os acessórios podem ser definidos uma vez e reutilizados em funções de teste, módulos, diretórios ou projetos.
-
Ativação automática: um recurso definido com
@pytest.fixture(autouse=True)é executado em todos os testes dentro de seu escopo de disponibilidade, sem precisar ser listado como argumento. Use essa opção com moderação porque oculta dependências de teste.
Criação de um acessório de arquivo temporário
Ao escrever testes que interagem com arquivos, é comum precisar de caminhos isolados que não entrem em conflito com outros testes ou dependam de um local embutido em código. O Pytest oferece recursos de diretórios temporários integrados, que geralmente são a melhor opção para novos testes, pois o Pytest cria diretórios exclusivos e gerencia sua retenção. No entanto, criar um pequeno recurso personalizado é uma maneira útil de compreender as solicitações de recurso e os valores de retorno.
O seguinte recurso retorna uma função de fábrica. A instalação em si não cria um arquivo durante a instalação. Em vez disso, a função aninhada create() cria um arquivo quando o teste chama tmp_file():
from pathlib import Path
import tempfile
import pytest
@pytest.fixture
def tmp_file():
def create(contents=""):
temp = tempfile.NamedTemporaryFile(delete=False)
try:
path = Path(temp.name)
finally:
temp.close()
path.write_text(contents, encoding="utf-8")
return path
return create
O recurso utiliza a função tempfile.NamedTemporaryFile() do Python com delete=False para que o arquivo continue existindo após ser fechado. O identificador de arquivo é fechado antes que o teste receba o caminho, o que é importante quando os testes precisam reabrir ou excluir o arquivo, especialmente no Windows.
Aqui está um exemplo de como um teste pode utilizar o recurso:
def test_file(tmp_file):
path = tmp_file("ready")
assert path.read_text(encoding="utf-8") == "ready"
Nesta configuração, tmp_file é o nome da fixture, e o teste recebe a create() função retornada pela fixture. A chamada tmp_file() dentro do teste cria o arquivo, grava o conteúdo solicitado e retorna um pathlib.Path objeto para ele.
Note
Tenha cuidado ao combinar recursos com gerenciadores de contexto. Se um recurso retornar um caminho ou um identificador de dentro de um bloco with, o gerenciador de contexto poderá fechar ou excluir o recurso antes que o teste o utilize. Mantenha o gerenciador de contexto aberto yield enquanto o teste precisar do recurso gerenciado, ou feche o recurso intencionalmente antes de retornar e faça a limpeza posteriormente.
Gerenciamento do escopo
Por padrão, as fixtures do pytest usam o escopo function. Um recurso com escopo de função é criado quando um teste o solicita e é destruído ao final desse teste. Se o recurso utilizar código de desmontagem, o pytest executa esse código no final do escopo do recurso.
O Pytest também permite escopos de recurso mais amplos para otimizar o trabalho de configuração, que pode ser demorado. Os nomes dos escopos do recurso integrados são:
-
function: o escopo padrão. O recurso é criado uma vez por função de teste. -
class: o recurso é criado uma vez por classe de teste. -
module: o recurso é criado uma única vez para o módulo. -
package: o recurso é criado uma única vez para o pacote em que está definido e é desativado após o último teste nesse pacote, incluindo os testes em subpacotes e subdiretórios. -
session: o recurso é criado uma única vez para a sessão de testes.
Para casos avançados, scope também pode ser um callable que retorna um desses nomes de escopo. Pytest chama a função uma vez quando define o fixture e passa os argumentos de palavra-chave fixture_name e config. Consulte a documentação do pytest no escopo dinâmico para obter detalhes.
Nesse contexto, criado uma vez significa que o pytest armazena em cache o valor do fixture para esse escopo. Por exemplo, um recurso com escopo de módulo pode ser solicitado por vários testes em um mesmo módulo, mas esses testes recebem o mesmo valor do recurso armazenado em cache. Uma luminária parametrizada é a exceção: o pytest pode criar um novo valor para cada conjunto de parâmetros dentro do escopo.
Mantenha os recursos no escopo da função, a menos que compartilhar a configuração seja seguro e útil. Objetos mutáveis, conexões abertas, alterações de ambiente e arquivos podem vazar o estado entre testes quando são armazenados em cache por um escopo mais amplo.
Veja como o acessório tmp_file ficaria com um escopo de módulo:
from pathlib import Path
import tempfile
import pytest
@pytest.fixture(scope="module")
def tmp_file():
def create(contents=""):
temp = tempfile.NamedTemporaryFile(delete=False)
try:
path = Path(temp.name)
finally:
temp.close()
path.write_text(contents, encoding="utf-8")
return path
return create
Como essa luminária retorna uma função de fábrica, scope="module" armazena em cache a função de fábrica do módulo. A fábrica ainda cria um novo arquivo sempre que um teste chama tmp_file(). Caso a fixture tenha gerado e devolvido um caminho de arquivo diretamente, então cada teste no módulo receberá o mesmo caminho.
Gerenciamento de limpeza
As versões anteriores da instalação tmp_file criam arquivos temporários, mas não removem esses arquivos. O Pytest recomenda recursos yield para facilitar a limpeza do código. O código antes yield executa a instalação e fornece um valor para o teste. Código após yield é executado durante o teardown, mesmo quando o teste falha após a conclusão da configuração. Se um recurso yield for encerrado antes de chegar a yield, o pytest não executa o código de desmontagem após yield, mas ainda assim desmonta quaisquer recursos anteriores que tenham sido concluídos com sucesso para o mesmo teste.
A versão a seguir rastreia todos os arquivos criados pela fábrica e tenta excluir esses arquivos após a conclusão do teste:
from pathlib import Path
import tempfile
import pytest
@pytest.fixture
def tmp_file():
paths = []
def create(contents=""):
temp = tempfile.NamedTemporaryFile(delete=False)
try:
path = Path(temp.name)
finally:
temp.close()
paths.append(path)
path.write_text(contents, encoding="utf-8")
return path
yield create
for path in paths:
try:
path.unlink()
except FileNotFoundError:
pass
Com o escopo padrão function , o pytest executa o código de limpeza após cada teste que solicita tmp_file. Se você alterar o recurso para scope="module", o pytest executará a limpeza durante a desativação do módulo, após concluir a execução dos testes nesse módulo. Quando há vários recursos yield ativos, o pytest executa o código de desmontagem na ordem inversa à da configuração.
A limpeza usa try/except FileNotFoundError porque um teste pode excluir o próprio arquivo. No Windows, a exclusão também pode falhar com PermissionError se um teste deixar o arquivo aberto; portanto, certifique-se de que os testes fechem todos os identificadores que criarem. Você também pode usar request.addfinalizer() para limpeza quando precisar de mais controle. Registre o finalizador somente após a criação do recurso, pois o pytest executa um finalizador adicionado durante a desmontagem, mesmo que o fixture venha a gerar uma exceção posteriormente. Para facilitar a limpeza do código, os recursos yield costumam ser mais fáceis de ler.
Usando conftest.py
Em vez de definir as fixtures em cada arquivo de teste, você pode salvar as fixtures compartilhadas em um arquivo chamado conftest.py. Os recursos em conftest.py ficam automaticamente disponíveis para os testes no mesmo diretório e em seus subdiretórios, sem a necessidade de importações explícitas.
Você pode ter vários conftest.py arquivos em um conjunto de testes. O Pytest determina a disponibilidade dos recursos a partir da perspectiva do teste solicitante: os recursos na classe ou módulo de teste são considerados nesse escopo local; em seguida, são considerados os recursos de arquivos conftest.py no mesmo diretório e nos diretórios superiores. Os testes podem procurar elementos de nível superior, mas não procuram em diretórios irmãos ou filhos. Os recursos de plug-in são pesquisados após os dispositivos locais. Não importe a partir de conftest.py nos arquivos de teste; deixe que o pytest a descubra.
Explorando dispositivos integrados
O Pytest tem muitos fixtures integrados projetados para simplificar os testes. Esses utilitários cuidam das tarefas comuns de preparação e limpeza, para que você possa se concentrar em escrever as asserções de teste. Para consultar a lista completa, consulte a documentação de referência do pytest sobre recursos integrados.
As principais luminárias internas incluem:
-
cache: armazena e recupera valores em execuções de pytest. -
capsys: captura texto escrito emstdoutestderr. -
tmp_path: fornece umpathlib.Pathobjeto para um diretório temporário exclusivo para cada invocação de função de teste, incluindo cada caso parametrizado. -
tmpdir: fornece um diretório temporário como um objeto herdadopy.path.local. Para novos testes, o pytest recomendatmp_path. -
monkeypatch: modifica temporariamente classes, funções, dicionários, variáveis de ambiente e outros objetos. -
request: fornece informações sobre o teste ou a instalação em execução no momento e dá suporte a finalizadores.
Outras funcionalidades internas comumente usadas incluem caplog (captura registros de log), capfd (captura a saída no nível do descritor de arquivo), tmp_path_factory (cria diretórios temporários com escopo de sessão) e pytestconfig (expõe a configuração ativa do pytest). Para ver a lista completa, consulte a documentação de referência do pytest sobre recursos integrados.
O próximo exercício usa tmp_path e pathlib.Path para criar arquivos temporários em um diretório específico para cada teste. Você pode encontrar tmpdir em projetos pytest existentes, mas para um novo código, prefira tmp_path. Por padrão, o pytest mantém diretórios temporários das últimas execuções de teste, portanto, ver pastas temporárias gerenciadas por pytest recentes após uma execução é esperada. Para obter mais informações, consulte a documentação pytest para arquivos e diretórios temporários.
A função do monkeypatching nos testes
Os recursos anteriores criaram e limparam os recursos de teste. Outro uso comum de luminárias é a modificação temporária de estado em testes que precisam isolar o código do estado externo. Nesses casos, o pytest oferece o recurso integrado monkeypatch, que é, por si só, um recurso do pytest e um bom exemplo de desinstalação automática.
O código de teste que depende de recursos externos, configuração global ou estado do sistema operacional pode ser desafiador. O recurso monkeypatch permite alterar temporariamente atributos, valores de dicionário, variáveis de ambiente, o diretório de trabalho atual ou sys.path para fins de teste. O Pytest desfaz automaticamente essas alterações após a conclusão da função de teste ou do fixture solicitado.
Aqui está um exemplo que substitui um atributo de função em um pequeno módulo de projeto para um teste. Aplicar patch em código de aplicação de escopo estreito (em vez de funções da biblioteca padrão usadas pelo próprio pytest) mantém o teste focado e evita quebrar acidentalmente o comportamento do próprio pytest.
# project_module.py
def get_user_id():
# Imagine this calls a remote service.
return "real-user"
# test_project.py
import project_module
def test_user_id(monkeypatch):
monkeypatch.setattr(project_module, "get_user_id", lambda: "test-user")
assert project_module.get_user_id() == "test-user"
Você também pode especificar o destino como uma cadeia de caracteres de caminho de importação com pontos. O Pytest resolve e importa o módulo nomeado quando aplica o patch, o que é conveniente quando o arquivo de teste ainda não precisa do módulo. O último import project_module neste exemplo é apenas para que o teste possa referenciar a função corrigida para a afirmação:
# test_project.py
def test_user_id_by_path(monkeypatch):
monkeypatch.setattr("project_module.get_user_id", lambda: "test-user")
import project_module
assert project_module.get_user_id() == "test-user"
Além de definir atributos e sobrescrever métodos com setattr(), o recurso monkeypatch pode excluir atributos com delattr(), definir e excluir variáveis de ambiente (setenv, delenv), definir e excluir itens do dicionário (setitem, delitem), colocar antes de sys.path (syspath_prepend), alterar o diretório de trabalho atual (chdir), e criar um escopo de aplicação de patches aninhado com context(). O comportamento padrão raising=True para setattr() e delattr() ajuda a capturar destinos com ortografia incorreta. Use monkeypatch.context() quando for necessário reverter uma alteração antes que o restante do desmontagem do teste ou do recurso continue. Para obter mais informações, consulte a documentação do pytest sobre monkeypatching.
Embora monkeypatch seja poderoso, use-o com cuidado:
-
Clareza e manutenção de código: o uso
monkeypatchexcessivo ou o uso dele de maneiras complexas pode tornar os testes mais difíceis de entender e manter. - Validade do teste: Monkeypatching pode criar testes que passam em condições artificiais muito diferentes do comportamento de produção.
- Dependência excessiva dos detalhes da implementação: testes que corrijam detalhes de implementação interna podem ficar frágeis quando o código subjacente é alterado.
-
Complexidade de depuração: os testes de depuração que usam
monkeypatchpodem ser mais difíceis quando um patch altera o comportamento fundamental do aplicativo.
Use monkeypatch quando ele ajuda a isolar um comportamento focado e manter cada patch o mais pequeno e explícito possível.