Trilha de aprendizado · Nível 15 · Tutorial 8

Testar a aplicação instalada em um ambiente limpo

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.

  • Nível: Avançado
  • Duração: 25 min
  • 8 passos
Testar a aplicação instalada em um ambiente limpo

O que você vai percorrer

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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

O que você vai aprender

  • Instalar a wheel em um ambiente novo sem recorrer à instalação editável.
  • Executar o comando instalado em outro processo com diretório e ambiente controlados.
  • Verificar resultados, mensagens, códigos de saída e efeitos persistentes da CLI.
  • Detectar dependências não declaradas e importações que só funcionam junto ao código-fonte.

Antes de começar

  • Gerar e inspecionar wheel e sdist
  • Traduzir falhas em mensagens e códigos de saída
  • Verificar integração com arquivos temporários
  • Criar ambientes virtuais com venv

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.

O objeto da validação

A pergunta correta

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.

Duas evidências diferentes

O fluxo de validação deve atravessar o artefato gerado, e não reutilizar diretamente os fontes do projeto.

Diagrama comparando testes de funções importadas da árvore de fontes com teste de uma wheel instalada em ambiente separado e executada pelo comando da CLI.

À 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á.

Dois ambientes, responsabilidades distintas

Não misture quem testa com quem é testado

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

O que pode mascarar um defeito

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.

Alcance real da evidência

Isolamento útil, não absoluto

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.

Escolha o desenho que valida a entrega

Qual teste produz a evidência mais forte?

Qual desenho verifica melhor se a aplicação distribuída funcionará para o usuário?

Passo 2 de 8

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.

Chame o processo sem um shell

Uma lista preserva os argumentos

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.

Argumentos chegam separados ao processo

A lista é transmitida como unidades distintas, inclusive quando um caminho contém espaços.

Diagrama mostrando uma lista Python com o executável, uma opção e um caminho com espaços, cada item chegando como um argumento separado ao processo, sem shell intermediário.

Não use algo como "arquivo com espaços.toml" dentro do elemento da lista: as aspas seriam caracteres literais do argumento.

Execução curta com captura

O caminho do executável será localizado no ambiente isolado em um step posterior. Por enquanto, observe o formato da chamada.

python
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 == ""

Leia o contrato do processo

CompletedProcess contém as evidências

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

Sucesso é um conjunto de condições

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

Timeout proporcional à ação

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.

Escolha check conforme o cenário

Falha obrigatória versus falha prevista

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.

Associe a situação à política

Relacione cada situação à política mais adequada de check.

Toque em um item e depois no par correspondente.

Diferencie os três tipos de falha

Nem toda falha vira um returncode

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.

Tratamento diagnóstico na infraestrutura do teste

Este padrão é útil no helper que executará a CLI nos próximos steps.

python
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.stderr

Decida para uma falha esperada

Você 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

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.

Uma instalação que a suíte pode provar

O artefato é uma entrada explícita

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.

Dois ambientes, duas responsabilidades

Diagrama mostrando o pytest em um ambiente de testes separado, que cria um venv temporário; a wheel é instalada nesse venv, que contém o Python e o comando da aplicação.

O pytest prepara o ambiente; a aplicação é instalada e executada somente dentro do venv temporário.

Dica

Escopo certo

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.

Fixture para criar e instalar

conftest.py — preparação compartilhada

Adapte meucomando ao nome declarado em [project.scripts] no seu pyproject.toml.

python
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)

Por que cada executável é explícito

Não ative o venv; use seus caminhos

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

Uso no primeiro teste

def test_ajuda(app_instalada) -> None:
    resultado = subprocess.run(
        [str(app_instalada.comando), "--help"],
        capture_output=True,
        text=True,
        timeout=10,
    )
    assert resultado.returncode == 0

A 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.

Sequência da preparação

Ordene o fluxo

Coloque as ações na ordem adequada para preparar a aplicação instalada.

  1. Localizar o Python dentro de `bin/` ou `Scripts/` conforme o sistema operacional.
  2. Criar um diretório temporário de sessão e executar `sys.executable -m venv <diretório>`.
  3. Executar `<python-do-venv> -m pip install <caminho-da-wheel>` e localizar o comando instalado.
  4. Receber `--wheel`, resolver o caminho e confirmar que ele é um arquivo `.whl`.

Passo 4 de 8

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.

Um cenário deve ter seu próprio espaço

Execute fora do projeto

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.

Fronteiras da execução

Diagrama mostrando o projeto e o ambiente de testes separados de um diretório temporário de cenário. A CLI instalada recebe caminhos absolutos, cwd temporário e variáveis de ambiente controladas.

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

Caminhos relativos

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.

Controle o ambiente sem apagar o sistema

Parta do ambiente atual e remova interferências

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

Não confunda isolamento com máquina nova

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.

Monte um ambiente controlado

Adapte os nomes MINHA_APP_CONFIG e MINHA_APP_DATA_DIR para as variáveis que sua aplicação realmente consulta.

python
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 env

Centralize a execução do comando real

Um helper sem importar a aplicação

O 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.

Helper para subprocess.run

python
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

Política de término

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.

Prática: descreva sua execução isolada

Quais controles você aplicou?

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

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.

Ajuda é um contrato do comando instalado

Teste o executável que o usuário recebe

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.

Dois processos, um artefato distribuído

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.

Diagrama mostrando pytest fora do ambiente da aplicação, iniciando um comando instalado em um ambiente virtual isolado; o comando produz saída e acessa dados temporários.

O teste observa o contrato externo do comando instalado.

Exemplo: ajuda pelo ponto de entrada

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.

python
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 == ""

Defina um cenário de sucesso observável

Dados conhecidos, expectativa independente

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.

Exemplo: executar e conferir todos os canais

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.

python
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}
    ]

Confirme que o efeito atravessa processos

Uma nova execução deve encontrar o estado gravado

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.

Segunda execução sobre os mesmos dados

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.

python
    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

O que torna a evidência forte

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 de processo

Contrato de sucesso

Complete a asserção que prova que o comando terminou com sucesso:

assert resultado._____ == 0

Passo 6 de 8

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.

Falha prevista também tem contrato

Não basta o comando falhar

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.

Evidências de uma falha tratada

Fluxo visual: uma configuração temporária inválida e um arquivo bloqueando um caminho chegam ao comando instalado; o resultado esperado é código não zero, diagnóstico em stderr e arquivo de dados inalterado.

Teste o processo, suas saídas e o estado persistente — não apenas a presença de uma palavra na mensagem.

Atenção

Não use permissões como única barreira

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.

Configuração inválida, demais condições válidas

Isole a causa

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.

Asserções para uma configuração inválida

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.

python
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_inicial

Exemplo

O que essa evidência demonstra

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.

Falha de acesso sem depender de permissões

Faça um arquivo bloquear um diretório

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.

Verificar erro e ausência de efeito persistente

Neste exemplo, a opção --diretorio-dados representa a forma prevista no contrato da aplicação para escolher o local dos dados.

python
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 acople o teste ao sistema operacional

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.

Reconheça um cenário determinístico

Escolha do cenário

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

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.

Localize a etapa que falhou

Siga a sequência de evidências

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.

Mapa de diagnóstico

Diagrama em sequência mostrando uma wheel entrando em um ambiente virtual limpo e cinco etapas: criar ambiente, instalar, localizar comando, iniciar comando e executar operação. As etapas finais se ramificam para módulo ausente, recurso ausente e interferência de ambiente.

Associe a saída capturada pelo teste à primeira etapa que não foi concluída, antes de decidir a correção.

Dica

Registre o contexto da evidência

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.

Teste hipóteses sem contaminar o ambiente

Módulo ausente não tem uma causa única

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.

Verificação complementar das dependências declaradas

Execute usando o interpretador do venv da aplicação, não o Python que roda o pytest.

python
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.stdout

Exemplo

O alcance de pip check

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 “conserte” o teste instalando algo à mã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.

Escolha a próxima verificação

Relacione a evidência à próxima ação

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

Diagnóstico que leva a uma correção válida

  • Identifique a primeira etapa que falhou antes de formular uma hipótese.
  • Cruze a saída do processo com o conteúdo da wheel e seus metadados; uma mensagem isolada não prova a causa.
  • Use pip check como evidência complementar, sabendo que ele não encontra dependências omitidas.
  • Depois de corrigir a distribuição, reconstrua a wheel e crie outro ambiente limpo para validar novamente.

Passo 8 de 8

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.

O que a suíte final demonstra

Uma evidência sobre a distribuição

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;
  • uma operação válida confirma resultado, mensagens e persistência;
  • uma configuração inválida confirma o erro previsto;
  • um caminho de dados impossível confirma que a falha não altera arquivos indevidamente.

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.

Fluxo da validação

A imagem resume as fronteiras que a suíte deve preservar.

Diagrama mostrando pytest em um ambiente de desenvolvimento criando um venv temporário, instalando uma wheel específica nele e executando quatro subprocessos da CLI em diretórios temporários externos ao projeto.

O pytest fica fora do ambiente da aplicação; cada cenário usa arquivos e diretório de trabalho próprios.

Dica

Conclusão proporcional

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.

Arquivo de suporte e preparação isolada

Crie o arquivo de teste

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.

tests/test_wheel_aceitacao.py — preparação

python
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,
    )

Os quatro cenários de aceitação

Complete os dados do seu contrato

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.

tests/test_wheel_aceitacao.py — cenários

python
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()

Executar, interpretar e registrar

Execute contra uma wheel explícita

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.

Revisão da sua execução

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

Rotina reproduzível concluída

Você consolidou a validação local da distribuição.

  • Passe à suíte o caminho explícito da wheel que deseja avaliar.
  • Mantenha pytest e ferramentas de desenvolvimento fora do venv temporário da aplicação.
  • Execute o ponto de entrada instalado com cwd, ambiente e timeout controlados.
  • Avalie códigos de saída, stdout, stderr e arquivos persistidos em conjunto.
  • Reconstrua a wheel e recrie o ambiente após corrigir defeitos de distribuição.
  • A rotina local poderá ser orquestrada posteriormente, sem mudar as evidências que ela produz.

Tutorial concluído

Parabéns! Você concluiu: Testar a aplicação instalada em um ambiente limpo

Muito bem! Agora você tem uma suíte local que testa a wheel como um usuário a receberia: instalada em um venv novo, executada fora das fontes e verificada por resultados observáveis. No próximo tutorial, essa rotina será orquestrada.

Baixe o Aplicativo agora para ter acesso a + de 5000 cursos gratuitos, exercícios, certificado e muito conteúdo sem pagar nada!

  • Cursos online 100% gratuitos do início ao fim

    Milhares de cursos online em vídeo, ebooks e áudiobooks.

  • Mais de 60 mil exercícios gratuitos

    Para testar seus conhecimentos no decorrer dos cursos online

  • Certificado Digital gratuito válido em todo o Brasil

    Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail

Aplicativo Cursa na tela de ebook, na tela de curso em vídeo e na tela de exercícios do curso, mais o certificado de conclusão de curso