A Anatomia de uma Suite pytest
Um teste pytest parece enganosamente simples - um nome de função começando com test_, uma instrução assert, pronto - mas quase todas as decisões não óbvias do pytest remontam a mecanismos que se escondem sob essa simplicidade: como as fixtures são realmente "encontradas" e injetadas, por que a coleta pode falhar antes que um único teste seja executado, e por que o escopo da fixture que você escolhe é realmente um trade-off entre isolamento e custo de setup.
Configuração do pytest inicia um projeto com fixtures e configuração reais; as páginas específicas da ferramenta que seguem - Fixtures, Parametrização, Mocking & Patching, Teste Baseado em Propriedades - cada uma se aprofunda em um mecanismo. Esta página permanece um nível acima: o que um teste realmente é sob o modelo do pytest, como as fixtures formam um grafo de dependência e como as peças se relacionam umas com as outras, independentemente de qual mecanismo específico um determinado teste usa.
Resumo
- pytest resolve as dependências de um teste através de injeção por nome de parâmetro, construindo um grafo de fixtures nos bastidores em vez de exigir ligação explícita de setup/teardown.
- Por que Importa: Entender a resolução e o escopo de fixtures explica quase todas as falhas confusas do pytest - uma fixture "não encontrada", estado compartilhado inesperado entre testes, ou uma suite que passa localmente, mas falha em uma ordem de coleta diferente.
- Conceitos Chave: coleta, fixture, escopo, injeção de dependência, parametrização, test double (duplo de teste).
- Quando Usar Este Modelo: Diagnosticar uma fixture que não está sendo encontrada ou está sendo compartilhada inesperadamente, decidir entre um mock e uma dependência real, escolher o escopo da fixture para um recurso caro, e explicar por que um erro de importação quebra toda a suite em vez de um único teste.
- Limitações / Trade-offs: A resolução implícita e baseada em nome de fixtures do pytest é poderosa, mas não óbvia - um grande grafo de fixtures através de muitos arquivos
conftest.pypode ser genuinamente difícil de rastrear sem ferramentas (pytest --fixtures), e nenhuma quantidade de testes verdes prova que os caminhos não testados estão corretos. - Tópicos Relacionados: a pirâmide de testes (unitária vs. integração), duplos de teste e limites de mocking, portões de integração contínua, teste baseado em propriedades.
Fundamentos
Antes que o pytest execute qualquer coisa, ele realiza a coleta: percorre os diretórios de teste, importa todos os arquivos que correspondem ao seu padrão de descoberta (test_*.py ou *_test.py), e reúne todas as funções cujo nome começa com test_ em uma lista de itens de teste. É por isso que um erro de sintaxe ou uma importação incorreta em um arquivo de teste quebra todo o passo de coleta em vez de falhar um teste - o arquivo precisa ser importado com sucesso antes que o pytest possa sequer ver o que está dentro dele.
Uma fixture é uma função decorada com @pytest.fixture que fornece a dependência de um teste - uma conexão de banco de dados, dados de exemplo, um cliente de API - e o pytest a injeta puramente combinando nomes de parâmetros: uma função de teste que declara def test_x(sample_user): recebe qualquer fixture sample_user que esteja visível no escopo, sem necessidade de importação ou ligação explícita. Essa resolução implícita e baseada em nome é o mecanismo de assinatura do pytest, e também é a fonte da maioria das confusões do tipo "por que minha fixture não está sendo usada", já que um erro de digitação no nome do parâmetro produz silenciosamente um erro de "fixture não encontrada" em vez de um no-op silencioso.
Uma analogia útil: pense em fixtures como um contêiner de injeção de dependência que resolve por nome em vez de por tipo - solicite db_session como um parâmetro, e o pytest percorre os arquivos conftest.py (o diretório atual, depois os diretórios pais) para encontrar a fixture mais próxima com esse nome exato, da mesma forma que um framework de DI em outra linguagem resolve um argumento de construtor para um provedor registrado.
import pytest
@pytest.fixture
def sample_user():
return {"id": 1, "email": "ada@example.com"}
def test_user_email(sample_user): # injetado por nome de parâmetro
assert "@" in sample_user["email"]Mecanismos e Interações
O escopo da fixture determina por quanto tempo o valor de uma fixture vive antes que o pytest a desmonte e crie uma nova, e escolher um escopo é realmente escolher um ponto em um trade-off entre isolamento e custo de setup. Uma fixture com escopo function (o padrão) é recriada para cada teste individual, maximizando o isolamento ao custo de repetir o trabalho de setup; uma fixture com escopo session é criada uma vez para toda a execução do teste, minimizando o custo, mas exigindo que o desenvolvedor garanta que nada modifique o estado compartilhado entre os testes que dependem dela. Um motor de banco de dados é o recurso textual de escopo session (caro de criar, seguro de compartilhar porque ele próprio não contém estado por teste), enquanto a transação que envolve cada teste geralmente permanece com escopo function especificamente para que as escritas de um teste nunca vazem para o próximo.
As fixtures se compõem simplesmente solicitando umas às outras como parâmetros, o que significa que a linguagem de "grafo de dependência" não é apenas uma metáfora - uma fixture que depende de outra fixture é uma aresta real em um grafo real que o pytest resolve antes de executar o teste, e o pytest impõe uma restrição rígida a esse grafo: uma fixture de escopo mais amplo não pode depender de uma de escopo mais estreito, porque uma fixture de escopo session criada uma vez não tem como receber um valor fresco de escopo function a cada teste.
Duplos de teste - substitutos para dependências reais - interagem com este mesmo grafo de fixtures em vez de substituí-lo: uma fixture monkeypatch ou uma chamada unittest.mock.patch substitui um nó no grafo de dependência (um cliente de rede, um relógio, uma variável de ambiente) para que um teste possa isolar a lógica que realmente importa. Escolher um duplo em vez de uma dependência real é uma versão menor do mesmo trade-off de isolamento versus realismo que as decisões de escopo fazem: um cliente HTTP mockado roda em microssegundos, mas não pode capturar um bug real de serialização, enquanto um cliente real contra um servidor de teste captura esse bug ao custo de um setup mais lento e complexo.
@pytest.fixture(scope="session")
def engine():
eng = create_engine("sqlite:///:memory:")
yield eng # teardown executa após o fim da sessão
eng.dispose()
@pytest.fixture
def db_session(engine): # depende da fixture de escopo de sessão
conn = engine.connect()
txn = conn.begin()
yield Session(bind=conn)
txn.rollback() # escopo de função: isola cada testeConsiderações Avançadas e Aplicações
Parametrização (@pytest.mark.parametrize) e teste baseado em propriedades (Hypothesis) estão no mesmo espectro subjacente em vez de serem recursos não relacionados: ambos executam um corpo de teste contra muitas entradas, mas a parametrização enumera uma lista fixa de casos escolhida pelo desenvolvedor, enquanto Hypothesis gera centenas de entradas a partir de uma estratégia declarada e reduz qualquer falha ao menor exemplo que a reproduz. A parametrização é a ferramenta certa quando os casos extremos já são conhecidos (um valor limite, uma string conhecida como ruim); Hypothesis justifica seu custo quando o domínio de entrada é grande o suficiente para que um humano enumerando casos manualmente preveja que perderá aquele que realmente quebra a função.
A arquitetura de plugins do pytest é o que transforma um núcleo relativamente pequeno (coleta, fixtures, reescrita de asserções) em um ecossistema que cobre código assíncrono, cobertura e teste de API web sem inchar a ferramenta base: pytest-asyncio ensina o runner a aguardar funções de teste corrotina, pytest-cov envolve coverage.py para relatar quais linhas foram executadas, e cada plugin se conecta à mesma maquinaria de coleta e fixtures em vez de inventar uma paralela. Essa arquitetura também é o motivo pelo qual misturar versões de plugins descuidadamente pode produzir falhas confusas - um plugin que se conecta ao setup de fixtures pode mudar o comportamento de fixtures que ele nunca toca diretamente.
Cobertura merece o mesmo escrutínio aqui que recebe em qualquer discussão de testes: ela relata a porcentagem de linhas ou ramificações que o pytest executou durante a execução, o que é uma afirmação completamente diferente do que foi significativamente verificado. Um teste que chama uma função e não afirma nada sobre seu resultado ainda conta como cobrindo todas as linhas que essa função executou enquanto não prova nada - a cobertura é útil para encontrar código que ninguém exercita, e um alvo pobre a ser perseguido por si só.
| Abordagem | Força | Fraqueza | Melhor Encaixe |
|---|---|---|---|
| Fixtures (escopo de função) | Isolamento máximo, sem estado vazado | Custo de setup pago em cada teste | Escolha padrão para a maioria das dependências de teste |
| Fixtures (escopo de sessão/módulo) | Amortiza setup caro uma vez | Requer cuidado para que nada modifique o estado compartilhado | Motores de banco de dados, instâncias de aplicativos, qualquer coisa cara e sem estado para compartilhar |
parametrize | Explícito, legível, rápido para casos conhecidos | Perde casos extremos que não foram enumerados | Valores limite, entradas conhecidas como ruins, pequenas matrizes |
| Hypothesis (baseado em propriedades) | Encontra casos extremos que ninguém enumerou, reduz falhas automaticamente | Mais lento, requer enquadrar invariantes em vez de exemplos | Parsers, serializadores, invariantes matemáticos/de ida e volta |
Conceitos Equivocados Comuns
- "Fixtures são apenas auxiliares de setup/teardown, como
setUp/tearDown." Elas são resolvidas através de um grafo de dependência baseado em nome que o pytest constrói e injeta automaticamente - mais próximo de injeção de dependência do que de código de setup imperativo. - "Uma fixture de escopo
sessioné sempre mais rápida e, portanto, sempre melhor." Ela amortiza o custo em toda a execução, mas apenas com segurança quando nada sobre ela é mutado entre os testes - mal aplicada a algo com estado por teste, ela quebra silenciosamente o isolamento em vez de economizar tempo. - "Mais casos parametrizados sempre significam melhor cobertura de casos extremos." Casos enumerados cobrem apenas o que um humano pensou em registrar; um grande domínio de entrada frequentemente tem um caso extremo que a parametrização nunca considerou, que é exatamente a lacuna que o Hypothesis foi projetado para fechar.
- "100% de cobertura significa que a suite é completa." A cobertura conta as linhas executadas, não as asserções que verificaram significativamente o comportamento - um teste pode executar todas as linhas e não verificar nada.
- "Um erro de importação em um arquivo de teste só falha os testes desse arquivo." A coleta precisa importar com sucesso um arquivo antes que o pytest possa ver qualquer teste dentro dele, então uma importação quebrada pode impedir o pytest de descobrir até mesmo testes que, de outra forma, passariam.
FAQs
Como o pytest realmente decide quais funções são testes?
Durante a coleta, ele importa todos os arquivos que correspondem ao seu padrão de descoberta (tipicamente test_*.py) e reúne todas as funções cujo nome começa com test_ - isso acontece antes que qualquer corpo de teste seja executado.
Como uma fixture é "injetada" em uma função de teste?
Combinando o nome do parâmetro da função de teste com uma fixture de mesmo nome visível no escopo - o pytest percorre os arquivos conftest.py para encontrar a correspondência mais próxima, sem necessidade de importação explícita.
Qual é a diferença prática entre os escopos de fixture?
O escopo controla por quanto tempo o valor de uma fixture vive antes de ser recriado - o escopo function o recria por teste para isolamento máximo, enquanto o escopo session o cria uma vez para toda a execução para amortizar um setup caro, ao custo de precisar garantir que ele permaneça seguro para compartilhar.
Por que uma fixture de escopo de sessão não pode depender de uma de escopo de função?
Porque a fixture de sessão é criada uma vez e não teria como receber um valor fresco de uma fixture destinada a ser recriada para cada teste individual - o pytest garante que um escopo mais amplo não possa depender de um mais estreito.
Qual é a diferença entre `yield` e `return` em uma fixture?
yield permite que o código após ele seja executado como teardown quando o teste (ou escopo) termina; return fornece um valor sem nenhuma fase de teardown.
Quando devo usar `parametrize` em vez de Hypothesis?
Use parametrize quando os casos extremos já são conhecidos e em pequeno número; recorra ao Hypothesis quando o domínio de entrada for grande o suficiente para que um humano enumerando casos manualmente provavelmente perca aquele que realmente quebra a função.
O que a "redução" do Hypothesis realmente faz?
Quando uma entrada gerada causa uma falha, o Hypothesis procura a menor entrada que ainda a reproduz, transformando um exemplo de falha grande e confuso em um exemplo mínimo e legível.
É sempre mais seguro usar mock de uma dependência do que a coisa real?
Não necessariamente - um mock isola um teste do custo e da instabilidade de uma dependência real, mas também não pode capturar um bug que apenas o comportamento da dependência real exporia, como um erro real de sintaxe SQL.
Por que um erro de importação em um arquivo de teste causa mais falhas do que o esperado?
A coleta precisa importar com sucesso um arquivo antes que o pytest possa identificar quaisquer testes dentro dele, portanto, uma importação quebrada pode impedir a descoberta de todos os testes nesse arquivo, não apenas aquele perto da linha quebrada.
O que a arquitetura de plugins do pytest realmente oferece?
Um conjunto compartilhado de hooks para coleta, fixtures e relatórios que plugins como pytest-asyncio ou pytest-cov estendem, em vez de cada plugin implementar sua própria maquinaria paralela de execução de testes.
Devo mirar em uma porcentagem específica de cobertura?
Não diretamente como um alvo - a cobertura informa qual código foi executado, não o que foi significativamente afirmado, portanto, é mais útil para encontrar código completamente não exercitado do que para julgar quão bem testado o código testado realmente é.
Por que o pytest recomenda `conftest.py` em vez de importar fixtures diretamente?
Os arquivos conftest.py são descobertos automaticamente em cada nível de diretório, então as fixtures definidas lá se tornam disponíveis para todos os testes nesse diretório e abaixo, sem uma importação explícita, que é o que faz o grafo de resolução baseado em nome funcionar em toda a árvore de testes.
Relacionados
- Configuração do pytest - layout do projeto, descoberta e configuração na prática
- Fixtures - escopos, fábricas e composição do grafo de dependência
- Parametrização - testes orientados a dados enumerados
- Mocking & Patching - substituição de nós no grafo de fixtures
- Teste Baseado em Propriedades - entradas geradas e redução
- Cobertura & Relatórios - o que a cobertura mede e seus limites
Versões da Stack: Esta página foi escrita para Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, e uv 0.6+.