
Passo 1 de 8
Definir o que o teste da instalação precisa provar
Determine qual artefato e quais fronteiras de ambiente fornecem evidência sobre a aplicação que será distribuída.
Trilha de aprendizado · Nível 15 · Tutorial 8
Escrever testes de integração que instalam a wheel em um ambiente novo e executam o comando real fora da pasta do projeto, detectando problemas ocultos pela instalação de desenvolvimento.
Definir o que o teste da instalação precisa provar
Determine qual artefato e quais fronteiras de ambiente fornecem evidência sobre a aplicação que será distribuída. 2 min
Executar processos e interpretar seus resultados
Inicie a CLI instalada como um processo separado e avalie seu resultado pelo contrato de saída, sem depender de um shell. 4 min
Preparar a instalação isolada dentro dos testes
Crie uma fixture de sessão que instala a wheel exata em um venv temporário e expõe os executáveis desse ambiente aos testes de aceitação. 4 min
Controlar diretório, ambiente e dados de cada execução
Isole cada execução da CLI instalada para que ela não encontre acidentalmente as fontes do projeto, configurações pessoais ou dados reais. 4 min
Verificar ajuda, resultado e persistência
Teste o ponto de entrada instalado e confirme um efeito persistente sem consultar a implementação da aplicação. 3 min
Testar falhas esperadas sem perder as evidências
Verifique falhas previstas da CLI instalada observando o contrato completo do processo e preservando o estado conhecido dos arquivos. 3 min
Diagnosticar o que só falha após a instalação
Investigue falhas da aplicação instalada por etapas e preserve a evidência de um ambiente limpo ao corrigir a distribuição. 3 min
Validar a wheel com uma suíte de aceitação completa
Consolide a preparação isolada e os cenários de aceitação para validar a wheel produzida, com evidências de processos e arquivos persistidos. 3 min

Passo 1 de 8
Determine qual artefato e quais fronteiras de ambiente fornecem evidência sobre a aplicação que será distribuída.
Testes executados pelo pytest podem provar que o código disponível no seu ambiente de desenvolvimento funciona. O teste de instalação responde outra pergunta: a wheel que será entregue instala e executa como aplicação?
Use a wheel recém-gerada como objeto explícito da validação. Informe seu caminho exato à suíte, por exemplo dist/minha_app-1.0.0-py3-none-any.whl. Não deixe o teste procurar automaticamente “alguma wheel” em dist/: um artefato antigo poderia ser escolhido silenciosamente.
O fluxo de validação deve atravessar o artefato gerado, e não reutilizar diretamente os fontes do projeto.

À esquerda, pytest alcança módulos do projeto; à direita, a wheel é instalada em outro ambiente e o ponto de entrada é executado como o usuário fará.
O ambiente que executa a suíte pode conter pytest, ferramentas de qualidade e dependências de desenvolvimento. Já o ambiente da aplicação deve receber somente a wheel e as dependências de execução declaradas por ela.
O comando a verificar é o ponto de entrada instalado pela wheel — não uma função importada diretamente pelo pytest. Assim, a evidência inclui metadados, dependências, arquivos distribuídos e a criação do executável.
Atenção
Uma instalação editável pode apontar para a árvore de fontes. Além disso, um PYTHONPATH herdado, o diretório atual dentro do projeto ou uma ferramenta já instalada podem fazer uma importação funcionar por acidente. Nessa situação, o teste passa, mas a distribuição entregue pode falhar.
Um venv novo reduz interferências de pacotes Python já instalados e separa a aplicação das ferramentas de teste. Ele não cria uma máquina nova: sistema operacional, arquitetura, interpretador disponível, variáveis externas e recursos do sistema ainda fazem parte do cenário.
Portanto, a conclusão correta é limitada: a wheel funciona nos cenários testados, naquele interpretador e naquele sistema, sob as condições controladas pela suíte.
Qual desenho verifica melhor se a aplicação distribuída funcionará para o usuário?

Passo 2 de 8
Inicie a CLI instalada como um processo separado e avalie seu resultado pelo contrato de saída, sem depender de um shell.
Em um teste de integração, execute o comando real com subprocess.run e uma lista de argumentos. Cada elemento da lista é um argumento já separado; não acrescente aspas de shell em valores que tenham espaços.
Use shell=False (o padrão, declarado aqui por clareza). Assim, o Python inicia o executável diretamente, sem pedir que um shell interprete caracteres especiais, aspas ou espaços.
A lista é transmitida como unidades distintas, inclusive quando um caminho contém espaços.

Não use algo como "arquivo com espaços.toml" dentro do elemento da lista: as aspas seriam caracteres literais do argumento.
O caminho do executável será localizado no ambiente isolado em um step posterior. Por enquanto, observe o formato da chamada.
from pathlib import Path
import subprocess
comando = Path("/caminho/para/venv/bin/minha-cli")
configuracao = Path("/tmp/cenario/configuração de teste.toml")
resultado = subprocess.run(
[str(comando), "--config", str(configuracao), "--ajuda"],
shell=False,
capture_output=True,
text=True,
check=False,
timeout=10,
)
assert resultado.returncode == 0
assert "uso:" in resultado.stdout.lower()
assert resultado.stderr == ""Com capture_output=True e text=True, subprocess.run devolve um CompletedProcess depois que o comando termina. Consulte returncode, stdout e stderr explicitamente.
text=True decodifica as saídas para str, o que permite verificar mensagens diretamente. A codificação e o ambiente controlado serão tratados adiante; aqui, o foco é não misturar os dois canais nem presumir sucesso só porque uma mensagem apareceu.
Exemplo
Para uma operação bem-sucedida, o teste costuma combinar evidências:
assert resultado.returncode == 0
assert "item criado" in resultado.stdout
assert resultado.stderr == ""Um stdout convincente não compensa returncode não zero. Da mesma forma, returncode == 0 não prova sozinho que a CLI produziu o resultado contratado.
Dica
Todo processo deve ter timeout finito. Uma instalação pode precisar de um limite maior, como 120 segundos; um comando local curto da CLI normalmente deve ter um limite bem menor, como 10 segundos. Escolha valores realistas para o seu contrato e para evitar testes travados.
Use check=True quando a etapa precisa terminar com código zero para o teste continuar, como uma preparação obrigatória. Se ela retornar código não zero, subprocess.run lança CalledProcessError com as saídas capturadas.
Nos cenários de aceitação, principalmente quando a CLI deve recusar uma entrada inválida, use check=False. O processo termina normalmente; então o teste verifica o código não zero e o diagnóstico definido pelo contrato.
Relacione cada situação à política mais adequada de check.
Toque em um item e depois no par correspondente.
Um comando iniciado e encerrado com erro produz um CompletedProcess com returncode diferente de zero. Já um executável inexistente ou sem possibilidade de inicialização pode lançar FileNotFoundError (ou outro OSError) antes de existir processo. Se o prazo se esgota, subprocess.run lança TimeoutExpired.
Portanto, uma asserção sobre returncode vale apenas quando o processo realmente foi iniciado e terminou.
Este padrão é útil no helper que executará a CLI nos próximos steps.
import subprocess
try:
resultado = subprocess.run(
argumentos,
shell=False,
capture_output=True,
text=True,
check=False,
timeout=10,
)
except FileNotFoundError as erro:
raise AssertionError(f"Executável não foi iniciado: {erro}") from erro
except subprocess.TimeoutExpired as erro:
raise AssertionError(
f"Comando excedeu {erro.timeout} segundos: {erro.cmd}"
) from erro
# Só aqui é seguro avaliar o término do processo.
assert resultado.returncode == 0, resultado.stderrVocê testa uma opção inválida e o contrato determina código de saída 2 e uma mensagem em stderr. Qual chamada permite verificar essas evidências diretamente?

Passo 3 de 8
Crie uma fixture de sessão que instala a wheel exata em um venv temporário e expõe os executáveis desse ambiente aos testes de aceitação.
O objeto deste teste é uma wheel específica, não a árvore de fontes. Receba seu caminho pela opção --wheel do pytest e valide-o antes de criar o ambiente. Assim, a suíte não escolhe silenciosamente uma wheel antiga em dist/.
A fixture abaixo tem escopo de sessão: criar e instalar uma única vez é suficiente. Já os arquivos mutáveis de cada cenário continuarão em diretórios temporários próprios, criados pelos testes depois.

O pytest prepara o ambiente; a aplicação é instalada e executada somente dentro do venv temporário.
Dica
Use scope="session" apenas para a wheel instalada e os caminhos dos executáveis. Não use essa fixture para compartilhar configuração, arquivos de dados ou diretório de trabalho entre cenários.
Adapte meucomando ao nome declarado em [project.scripts] no seu pyproject.toml.
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
import subprocess
import sys
import pytest
@dataclass(frozen=True)
class AppInstalada:
python: Path
comando: Path
def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption(
"--wheel",
action="store",
required=True,
help="Caminho absoluto ou relativo da wheel a validar.",
)
@pytest.fixture(scope="session")
def app_instalada(
request: pytest.FixtureRequest,
tmp_path_factory: pytest.TempPathFactory,
) -> AppInstalada:
wheel = Path(request.config.getoption("--wheel")).resolve()
if not wheel.is_file() or wheel.suffix != ".whl":
pytest.fail(f"Wheel inválida ou inexistente: {wheel}")
venv_dir = tmp_path_factory.mktemp("venv-app")
subprocess.run(
[sys.executable, "-m", "venv", str(venv_dir)],
check=True,
capture_output=True,
text=True,
timeout=30,
)
if os.name == "nt":
python = venv_dir / "Scripts" / "python.exe"
comando = venv_dir / "Scripts" / "meucomando.exe"
else:
python = venv_dir / "bin" / "python"
comando = venv_dir / "bin" / "meucomando"
subprocess.run(
[str(python), "-m", "pip", "install", str(wheel)],
check=True,
capture_output=True,
text=True,
timeout=60,
)
if not comando.is_file():
pytest.fail(f"Comando instalado não encontrado: {comando}")
return AppInstalada(python=python, comando=comando)
sys.executable é o Python que está executando o pytest e cria o venv. Depois disso, o único Python usado para instalar a aplicação é venv_dir/bin/python no POSIX ou venv_dir/Scripts/python.exe no Windows.
A instalação chama python -m pip install <caminho-absoluto-da-wheel>. Isso instala a distribuição normalmente e permite que o pip resolva as dependências declaradas. Não acrescente -e, extras de desenvolvimento nem pytest nesse venv.
check=True faz a fixture falhar imediatamente se a criação ou a instalação der erro. Como a saída foi capturada, o pytest inclui stdout e stderr do processo na exceção, preservando evidências úteis.
Exemplo
def test_ajuda(app_instalada) -> None:
resultado = subprocess.run(
[str(app_instalada.comando), "--help"],
capture_output=True,
text=True,
timeout=10,
)
assert resultado.returncode == 0A política completa de cwd e env será adicionada no próximo step. Aqui, o ponto principal é que app_instalada.comando aponta para o executável criado pela wheel, não para algo encontrado no PATH.
Coloque as ações na ordem adequada para preparar a aplicação instalada.

Passo 4 de 8
Isole cada execução da CLI instalada para que ela não encontre acidentalmente as fontes do projeto, configurações pessoais ou dados reais.
Cada cenário de aceitação deve usar um diretório temporário próprio, criado pelo tmp_path, e passá-lo em cwd. Assim, qualquer caminho relativo usado pela CLI parte de um local sem arquivos-fonte, sem configuração do repositório e sem dados anteriores.
Use caminhos absolutos para o executável instalado, para o arquivo de configuração e para os dados do cenário. Não dependa da ativação do venv, do PATH atual nem do diretório de onde o pytest foi iniciado.

O pytest pode ficar no ambiente de desenvolvimento; a CLI executada deve trabalhar somente no cenário temporário e na instalação criada para ela.
Dica
Crie os arquivos do cenário a partir de tmp_path e use .resolve() ao passá-los ao processo. Isso torna explícito o que a aplicação pode acessar, mesmo que o comando mude o diretório de trabalho internamente.
Não passe env={}. O sistema pode precisar de variáveis como SystemRoot, PATH, TEMP ou TMP, especialmente no Windows. Em vez disso, copie os.environ, remova PYTHONPATH e PYTHONHOME, remova as variáveis específicas da sua aplicação e então defina apenas os valores do cenário.
Também redirecione diretórios pessoais para dentro do temporário. Isso impede que uma configuração em HOME, USERPROFILE, APPDATA ou LOCALAPPDATA influencie a execução.
Atenção
Um venv novo e um ambiente controlado reduzem interferências de pacotes Python, fontes e preferências pessoais. Eles não isolam todos os recursos do sistema operacional, como rede, permissões globais ou executáveis externos.
Adapte os nomes MINHA_APP_CONFIG e MINHA_APP_DATA_DIR para as variáveis que sua aplicação realmente consulta.
from __future__ import annotations
import os
from collections.abc import Iterable, Mapping
from pathlib import Path
def controlled_env(
sandbox: Path,
*,
app_variable_names: Iterable[str],
app_values: Mapping[str, str],
) -> dict[str, str]:
"""Cria um ambiente do processo sem herdar fontes ou perfil pessoal."""
sandbox = sandbox.resolve()
home = sandbox / "home"
home.mkdir(parents=True, exist_ok=True)
# Preserva as variáveis necessárias ao sistema e remove interferências.
env = os.environ.copy()
for name in ("PYTHONPATH", "PYTHONHOME", *app_variable_names):
env.pop(name, None)
# A captura e a saída do processo usam a mesma codificação.
env["PYTHONUTF8"] = "1"
env["PYTHONIOENCODING"] = "utf-8"
env["HOME"] = str(home)
if os.name == "nt":
env["USERPROFILE"] = str(home)
for name in ("APPDATA", "LOCALAPPDATA"):
directory = sandbox / name.lower()
directory.mkdir(exist_ok=True)
env[name] = str(directory)
# Estes valores são definidos pelo cenário, não pelo computador do aluno.
env.update(app_values)
return envO helper recebe o caminho do comando disponibilizado pela fixture de instalação e inicia outro processo. Ele não importa módulos da aplicação nem chama sua implementação: a evidência vem do executável instalado.
encoding="utf-8" combina com os arquivos escritos em UTF-8 e com as variáveis definidas antes. Dessa forma, stdout e stderr podem ser verificados como texto, inclusive em mensagens em pt-BR.
from __future__ import annotations
import subprocess
from collections.abc import Mapping, Sequence
from pathlib import Path
def run_cli(
installed_command: Path,
arguments: Sequence[str],
*,
cwd: Path,
env: Mapping[str, str],
check: bool = False,
timeout: float = 10,
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[str(installed_command.resolve()), *arguments],
cwd=cwd.resolve(),
env=dict(env),
shell=False,
capture_output=True,
text=True,
encoding="utf-8",
check=check,
timeout=timeout,
)
# Em um teste, construa todos os arquivos dentro de tmp_path:
# scenario = tmp_path / "operacao"
# scenario.mkdir()
# config = scenario / "config.toml"
# data_dir = scenario / "dados"
# data_dir.mkdir()
#
# env = controlled_env(
# scenario,
# app_variable_names=("MINHA_APP_CONFIG", "MINHA_APP_DATA_DIR"),
# app_values={
# "MINHA_APP_CONFIG": str(config.resolve()),
# "MINHA_APP_DATA_DIR": str(data_dir.resolve()),
# },
# )
# result = run_cli(installed_command, ["--help"], cwd=scenario, env=env)Dica
Use check=True em uma preparação que obrigatoriamente precisa terminar com sucesso. Para as chamadas da CLI que serão avaliadas como cenários de aceitação, mantenha check=False e examine depois o returncode, stdout e stderr conforme o contrato.
No seu projeto, faça uma chamada local ao comando instalado usando o helper. Relate quais controles impedem acesso às fontes e quais impedem que a configuração pessoal interfira.
Escreva pelo menos 120 caracteres (0/120).

Passo 5 de 8
Teste o ponto de entrada instalado e confirme um efeito persistente sem consultar a implementação da aplicação.
O teste de ajuda deve chamar o caminho absoluto do ponto de entrada instalado, usando o helper de execução preparado anteriormente. Não importe main nem outra função da aplicação: isso testaria o código disponível ao pytest, e não o comando distribuído.
Para a ajuda, verifique o código 0, alguns trechos estáveis do contrato e o canal de erro esperado. Evite comparar a ajuda inteira: a largura do terminal, quebras de linha e caminhos temporários podem variar.
O pytest inicia o executável instalado em outro processo. A evidência vem de returncode, stdout, stderr e dos arquivos observáveis — não de uma chamada direta à implementação.

O teste observa o contrato externo do comando instalado.
Adapte apenas o nome dos termos estáveis ao contrato da sua CLI. cli.command, run_cli e exec_env representam a fixture e o helper criados nos steps anteriores.
def test_ajuda_do_comando_instalado(cli, exec_env):
resultado = run_cli(
[str(cli.command), "--help"],
cwd=exec_env.cwd,
env=exec_env.env,
check=False,
timeout=10,
)
assert resultado.returncode == 0
assert "uso:" in resultado.stdout.lower()
assert "adicionar" in resultado.stdout
assert resultado.stderr == ""
Monte no diretório temporário os arquivos de configuração e os dados iniciais que o cenário exige. Em seguida, execute a operação com argumentos explícitos.
O valor esperado deve ser escrito pelo teste a partir do requisito, e não calculado ao importar regras da aplicação. No exemplo, a CLI tarefas cria uma tarefa em um arquivo JSON; adapte nomes de comando, configuração, formato e mensagens ao contrato do seu projeto.
Este cenário assume que --config aponta para um TOML e que o arquivo configurado armazena uma lista JSON. O caminho absoluto evita que a interpretação dependa do diretório atual.
import json
def test_adiciona_tarefa_na_instalacao(cli, exec_env):
configuracao = exec_env.cwd / "config.toml"
dados = exec_env.cwd / "dados" / "tarefas.json"
configuracao.write_text(
'[app]\narquivo = "dados/tarefas.json"\n',
encoding="utf-8",
)
resultado = run_cli(
[
str(cli.command),
"--config", str(configuracao),
"adicionar", "Estudar integração",
],
cwd=exec_env.cwd,
env=exec_env.env,
check=False,
timeout=10,
)
assert resultado.returncode == 0
assert "Tarefa adicionada" in resultado.stdout
assert resultado.stderr == ""
assert dados.exists()
conteudo = json.loads(dados.read_text(encoding="utf-8"))
assert conteudo == [
{"titulo": "Estudar integração", "concluida": False}
]
A primeira execução pode parecer correta mesmo que só mantenha dados na memória. Execute o comando novamente, no mesmo cwd, com a mesma configuração e o mesmo ambiente controlado. Se a segunda execução encontra o item esperado, o teste demonstra persistência entre processos.
Confira novamente código de saída, canais de saída e um trecho estável do resultado. Não compare o texto integral se a CLI inclui detalhes variáveis.
Inclua este trecho no fim do teste anterior ou em um teste que prepare seu próprio estado. Cada teste deve continuar tendo seu diretório temporário exclusivo.
consulta = run_cli(
[str(cli.command), "--config", str(configuracao), "listar"],
cwd=exec_env.cwd,
env=exec_env.env,
check=False,
timeout=10,
)
assert consulta.returncode == 0
assert "Estudar integração" in consulta.stdout
assert consulta.stderr == ""
estado_final = json.loads(dados.read_text(encoding="utf-8"))
assert estado_final == [
{"titulo": "Estudar integração", "concluida": False}
]
Dica
Uma mensagem de sucesso isolada não basta. O cenário bem-sucedido combina: término com código zero, stdout contratado, stderr vazio quando esse é o contrato, arquivo criado ou atualizado e nova execução que observa o estado persistido.
Complete a asserção que prova que o comando terminou com sucesso:
assert resultado._____ == 0

Passo 6 de 8
Verifique falhas previstas da CLI instalada observando o contrato completo do processo e preservando o estado conhecido dos arquivos.
Em um teste de aceitação, uma falha prevista é um resultado verificável. Crie o arquivo de configuração inválido no diretório temporário do cenário, mantenha os demais dados válidos e execute o comando instalado com check=False.
Então verifique o conjunto do contrato: returncode não zero esperado, diagnóstico útil em stderr, comportamento previsto de stdout e arquivos que não foram alterados. Um traceback em uma entrada inválida normalmente revela que a falha escapou da fronteira da CLI.

Teste o processo, suas saídas e o estado persistente — não apenas a presença de uma palavra na mensagem.
Atenção
Retirar permissões de um diretório pode produzir resultados diferentes conforme o sistema operacional, o sistema de arquivos e os privilégios do usuário. Para uma falha determinística, crie um arquivo em um componente do caminho que a aplicação precisa usar como diretório. Aplique asserções à mensagem da aplicação, e não à redação variável do erro do sistema operacional.
Monte o cenário para que somente a configuração seja inválida: use um arquivo temporário com uma chave ou valor que viola o contrato da aplicação; mantenha acessíveis o diretório de trabalho e os dados necessários. Assim, um código de erro de configuração representa a validação da CLI, não uma falha acidental de infraestrutura.
Adapte os nomes do argumento, do código e do trecho de mensagem ao contrato da sua CLI. run_cli é o helper controlado criado no step anterior.
def test_rejeita_configuracao_invalida(run_cli, tmp_path):
config = tmp_path / "invalida.toml"
dados = tmp_path / "dados.json"
dados.write_text('[]\n', encoding="utf-8")
estado_inicial = dados.read_bytes()
# Exemplo: "limite" deve ser inteiro segundo o contrato da aplicação.
config.write_text('[app]\nlimite = "muitos"\n', encoding="utf-8")
resultado = run_cli(
["--config", str(config), "listar"],
cwd=tmp_path,
check=False,
)
assert resultado.returncode == CODIGO_CONFIGURACAO_INVALIDA
assert "configuração" in resultado.stderr.lower()
assert "traceback" not in resultado.stderr.lower()
assert resultado.stdout == "" # Se este for o contrato da sua CLI.
assert dados.read_bytes() == estado_inicialExemplo
Se o teste passa, você demonstrou que o comando instalado recebeu uma configuração inválida, encerrou com o código contratado, informou o usuário pelo canal de erro e não modificou os dados conhecidos. A asserção "configuração" in stderr é deliberadamente mais estável do que comparar a frase inteira.
Escolha um local de dados configurável pela CLI ou pelo arquivo de configuração. Crie um arquivo comum onde a aplicação espera encontrar ou criar um diretório e peça uma operação que use um caminho abaixo dele. O sistema não consegue tratar simultaneamente esse componente como arquivo e diretório; isso torna o cenário mais reprodutível que uma mudança de permissão.
Neste exemplo, a opção --diretorio-dados representa a forma prevista no contrato da aplicação para escolher o local dos dados.
def test_relata_caminho_de_dados_inacessivel(run_cli, tmp_path):
bloqueio = tmp_path / "nao-e-diretorio"
bloqueio.write_text("sou um arquivo\n", encoding="utf-8")
estado_inicial = bloqueio.read_bytes()
destino = bloqueio / "registros.json"
resultado = run_cli(
["--diretorio-dados", str(destino), "adicionar", "café"],
cwd=tmp_path,
check=False,
)
assert resultado.returncode == CODIGO_ACESSO_DADOS
assert "dados" in resultado.stderr.lower()
assert "traceback" not in resultado.stderr.lower()
assert resultado.stdout == "" # Ajuste somente se o contrato prever outra saída.
assert bloqueio.read_bytes() == estado_inicial
assert not destino.exists()Dica
Não afirme que stderr contém expressões como “Not a directory” ou “Acesso negado”: elas pertencem ao sistema operacional e podem mudar. Verifique a mensagem estável que a sua aplicação produz ao traduzir a falha, como uma referência ao local de dados ou à operação impossível.
Para testar uma falha de acesso aos dados de forma reproduzível, remover permissões do diretório é sempre mais confiável do que criar um arquivo no lugar de um diretório necessário.

Passo 7 de 8
Investigue falhas da aplicação instalada por etapas e preserve a evidência de um ambiente limpo ao corrigir a distribuição.
Não trate toda falha como “problema de instalação”. Investigue na ordem em que o teste acontece: preparação do venv → instalação da wheel → localização do executável → inicialização da CLI → operação solicitada.
Uma falha ao criar o ambiente ou baixar uma dependência pode ser externa, como indisponibilidade do índice ou interpretador incompatível. Já uma falha ao executar uma operação depois de a instalação terminar pode revelar um defeito da distribuição.

Associe a saída capturada pelo teste à primeira etapa que não foi concluída, antes de decidir a correção.
Dica
Guarde o comando executado, returncode, stdout, stderr, o caminho absoluto da wheel e a etapa em que ocorreu a falha. Uma exceção isolada, sem esse contexto, raramente identifica a causa com segurança.
Um ModuleNotFoundError ao usar uma funcionalidade pode indicar uma dependência de execução que existe no seu ambiente de desenvolvimento, mas não foi declarada nos metadados. Também pode indicar que um módulo do próprio projeto não entrou na wheel.
Correlacione a mensagem com o conteúdo da wheel e com os metadados que você já inspecionou. Se a aplicação lê um modelo, um arquivo de dados ou outro recurso, verifique também se ele foi distribuído e se o código não está procurando esse recurso a partir do diretório de trabalho.
Execute usando o interpretador do venv da aplicação, não o Python que roda o pytest.
resultado = subprocess.run(
[str(app_python), "-m", "pip", "check"],
capture_output=True,
text=True,
check=False,
timeout=30,
)
assert resultado.returncode == 0, resultado.stderr or resultado.stdoutExemplo
Se pip check informar que um pacote declarado exige uma versão incompatível de outro pacote instalado, há uma inconsistência detectável entre dependências declaradas.
Mas se o projeto importa relatorio_externo em uma operação e esqueceu de declará-lo, pip check pode retornar sucesso: ele só avalia relações que já estão nos metadados instalados. O teste da operação real é que expõe o requisito omitido.
Atenção
Não instale manualmente no venv limpo a dependência que faltou para fazer o teste passar. Corrija a fonte ou os metadados, gere uma nova wheel e execute a suíte em um novo ambiente. Caso contrário, você valida um ambiente alterado, não o artefato distribuído.
Associe cada evidência à verificação que melhor preserva o diagnóstico do ambiente limpo.
Toque em um item e depois no par correspondente.
Resumo
pip check como evidência complementar, sabendo que ele não encontra dependências omitidas.
Passo 8 de 8
Consolide a preparação isolada e os cenários de aceitação para validar a wheel produzida, com evidências de processos e arquivos persistidos.
A suíte final não importa o código da sua árvore de fontes. Ela recebe o caminho explícito da wheel, cria um venv novo, instala essa wheel e chama o executável instalado em processos separados.
Os quatro cenários formam uma evidência conjunta:
--help confirma que o ponto de entrada existe e responde;Antes de copiar o código, ajuste somente o bloco CONTRATO DO SEU PROJETO aos nomes do comando, argumentos, códigos de saída e formato de dados que você definiu nos tutoriais anteriores.
A imagem resume as fronteiras que a suíte deve preservar.

O pytest fica fora do ambiente da aplicação; cada cenário usa arquivos e diretório de trabalho próprios.
Dica
Uma suíte aprovada demonstra os cenários executados para esta wheel, neste interpretador e neste sistema operacional. Ela não prova compatibilidade universal nem cobre operações que você não exercitou.
No ambiente que contém o pytest, crie tests/test_wheel_aceitacao.py. O primeiro bloco registra a opção --wheel, valida o artefato, cria o venv fora do projeto e localiza os executáveis sem ativar o ambiente.
No bloco de contrato abaixo, os valores representam uma CLI de inventário apenas como exemplo completo. Troque-os pelos valores estáveis do contrato da sua aplicação; não calcule resultados chamando módulos da aplicação.
from __future__ import annotations
import json
import os
from pathlib import Path
import subprocess
import sys
import pytest
# ===== CONTRATO DO SEU PROJETO: ajuste estes valores uma única vez =====
COMMAND = "inventario"
SUCCESS_ARGS = ["adicionar", "cafe", "3"]
SUCCESS_STDOUT = "Item adicionado: cafe\n"
INVALID_CONFIG_EXIT = 3
DATA_ACCESS_EXIT = 4
# ======================================================================
def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption(
"--wheel",
action="store",
required=True,
help="Caminho da wheel exata que será instalada no venv temporário.",
)
@pytest.fixture(scope="session")
def wheel_path(pytestconfig: pytest.Config) -> Path:
wheel = Path(pytestconfig.getoption("wheel")).expanduser().resolve()
if not wheel.is_file() or wheel.suffix != ".whl":
pytest.fail(f"Wheel inválida ou inexistente: {wheel}")
return wheel
@pytest.fixture(scope="session")
def installed_app(tmp_path_factory: pytest.TempPathFactory, wheel_path: Path) -> dict[str, Path]:
root = tmp_path_factory.mktemp("app_instalada")
venv_dir = root / "venv"
create = subprocess.run(
[sys.executable, "-m", "venv", str(venv_dir)],
capture_output=True,
text=True,
timeout=60,
check=False,
)
if create.returncode != 0:
raise RuntimeError(
"Não foi possível criar o venv da aplicação.\n"
f"stdout:\n{create.stdout}\nstderr:\n{create.stderr}"
)
scripts = venv_dir / ("Scripts" if os.name == "nt" else "bin")
python = scripts / ("python.exe" if os.name == "nt" else "python")
command = scripts / (f"{COMMAND}.exe" if os.name == "nt" else COMMAND)
install = subprocess.run(
[str(python), "-m", "pip", "install", str(wheel)],
capture_output=True,
text=True,
timeout=120,
check=False,
)
if install.returncode != 0:
raise RuntimeError(
"A instalação da wheel falhou.\n"
f"stdout:\n{install.stdout}\nstderr:\n{install.stderr}"
)
if not command.is_file():
pytest.fail(f"Ponto de entrada instalado não encontrado: {command}")
return {"python": python, "command": command}
@pytest.fixture
def workspace(tmp_path: Path) -> Path:
"""Um diretório novo por cenário, nunca a raiz do projeto."""
return tmp_path
def controlled_env() -> dict[str, str]:
env = os.environ.copy() # preserva variáveis necessárias ao sistema, especialmente no Windows
for name in (
"PYTHONPATH", "PYTHONHOME", "INVENTARIO_CONFIG",
"INVENTARIO_DATA_DIR", "HOME", "USERPROFILE",
):
env.pop(name, None)
env["PYTHONIOENCODING"] = "utf-8"
return env
def run_cli(
installed_app: dict[str, Path],
workspace: Path,
*args: str,
timeout: int = 15,
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[str(installed_app["command"]), *args],
cwd=workspace,
env=controlled_env(),
capture_output=True,
text=True,
encoding="utf-8",
errors="strict",
timeout=timeout,
check=False,
)
O exemplo usa config.toml, um arquivo JSON e o comando fictício inventario adicionar cafe 3. Se a sua aplicação tiver outra interface, mantenha a estrutura dos testes e substitua os conteúdos, argumentos e expectativas pelos critérios de aceitação do seu projeto.
Os cenários de falha usam check=False porque o código não zero é a evidência esperada. A preparação, em contraste, falha imediatamente se criar o venv ou instalar a wheel não funcionar.
def write_valid_config(workspace: Path, data_path: Path) -> Path:
config = workspace / "config.toml"
config.write_text(
"[app]\n"
f'data_path = "{data_path}"\n',
encoding="utf-8",
)
return config
def test_ajuda_do_comando_instalado(installed_app: dict[str, Path], workspace: Path) -> None:
result = run_cli(installed_app, workspace, "--help")
assert result.returncode == 0
assert "usage:" in result.stdout.lower()
assert "adicionar" in result.stdout
assert result.stderr == ""
def test_operacao_persiste_entre_processos(
installed_app: dict[str, Path], workspace: Path
) -> None:
data = workspace / "dados.json"
data.write_text('{"itens": []}', encoding="utf-8")
config = write_valid_config(workspace, data)
first = run_cli(
installed_app, workspace, *SUCCESS_ARGS, "--config", str(config)
)
assert first.returncode == 0
assert first.stdout == SUCCESS_STDOUT
assert first.stderr == ""
assert json.loads(data.read_text(encoding="utf-8")) == {
"itens": [{"nome": "cafe", "quantidade": 3}]
}
# Um segundo processo deve observar o estado persistido pelo primeiro.
second = run_cli(installed_app, workspace, "listar", "--config", str(config))
assert second.returncode == 0
assert "cafe" in second.stdout
assert "3" in second.stdout
assert second.stderr == ""
def test_configuracao_invalida_tem_diagnostico_contratado(
installed_app: dict[str, Path], workspace: Path
) -> None:
config = workspace / "invalido.toml"
config.write_text("[app\ndata_path = [", encoding="utf-8")
result = run_cli(installed_app, workspace, "listar", "--config", str(config))
assert result.returncode == INVALID_CONFIG_EXIT
assert "configura" in result.stderr.lower()
assert result.stdout == ""
assert "traceback" not in result.stderr.lower()
assert not (workspace / "dados.json").exists()
def test_caminho_de_dados_bloqueado_nao_altera_estado(
installed_app: dict[str, Path], workspace: Path
) -> None:
blocker = workspace / "nao_e_diretorio"
blocker.write_text("sou um arquivo", encoding="utf-8")
impossible_data_path = blocker / "dados.json"
config = write_valid_config(workspace, impossible_data_path)
result = run_cli(
installed_app, workspace, *SUCCESS_ARGS, "--config", str(config)
)
assert result.returncode == DATA_ACCESS_EXIT
assert "dados" in result.stderr.lower()
assert result.stdout == ""
assert "traceback" not in result.stderr.lower()
assert blocker.read_text(encoding="utf-8") == "sou um arquivo"
assert not impossible_data_path.exists()
Na raiz do projeto, com o ambiente de desenvolvimento que contém pytest ativo, informe a wheel recém-gerada pelo caminho explícito. Ajuste o nome do arquivo para a versão que você acabou de construir:
python -m pytest -q tests/test_wheel_aceitacao.py --wheel "dist/seu_pacote-1.0.0-py3-none-any.whl"
Uma aprovação dos quatro testes mostra: a wheel foi instalada sem modo editável; o executável veio do venv temporário; a CLI respondeu nos quatro contratos; e o cenário de sucesso persistiu dados entre processos. Se a preparação falhar, examine primeiro a criação do venv, a instalação da wheel e a presença do executável. Se apenas uma operação falhar, use stdout, stderr, returncode e o estado dos arquivos para localizar a etapa afetada.
Após executar a suíte, relate: qual wheel foi validada, o resultado dos quatro cenários, uma evidência de persistência e um limite explícito da sua conclusão. Se algum cenário falhou, descreva a etapa e a evidência observada.
Escreva pelo menos 180 caracteres (0/180).
Resumo
Você consolidou a validação local da distribuição.
Parabéns! Você concluiu: Testar a aplicação instalada em um ambiente limpo
Milhares de cursos online em vídeo, ebooks e áudiobooks.
Para testar seus conhecimentos no decorrer dos cursos online
Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail
Baixe nosso aplicativo pelo QR Code ou pelos links abaixo:.
+ de 10 milhões
de alunos
Certificado grátis e
válido em todo o Brasil
60 mil exercícios
gratuitos
4,8/5 classificação
nas lojas de apps
Cursos gratuitos em
vídeo, ebooks e audiobooks