Para testar latência e consumo de tokens com pytest, separe duas coisas: a lógica que mede e aplica limites, testada sem rede, e a chamada real à API, executada de forma controlada. O primeiro conjunto precisa ser previsível. O segundo precisa registrar contexto suficiente para distinguir uma regressão do seu código de uma oscilação externa.
O objetivo é descobrir se uma mudança de prompt, montagem de contexto ou política de tentativas piorou o contrato da aplicação. Isso difere de acompanhar o serviço continuamente ou gerar tráfego de carga. Um teste de regressão pergunta se duas versões comparáveis continuam dentro de um orçamento previamente definido.
Resumo rápido
- Use relógio monotônico para duração e os campos de uso retornados pela API para consumo.
- Mocks validam a instrumentação; somente chamadas reais observam o comportamento do fornecedor.
- Fixe casos, parâmetros, ambiente e política de tentativas antes de comparar versões.
- Separe falha funcional, timeout, limite de tokens e degradação estatística.
- Defina o custo máximo da suíte e preserve os registros de cada execução.
1. Defina o contrato que o teste protege
Comece por uma tarefa pequena: classificar uma mensagem, extrair campos de um pedido ou resumir um documento conhecido. Registre entrada, versão do prompt, identificador do modelo configurado, parâmetros de geração e tamanho esperado da saída. Se o teste muda a entrada a cada execução, fica difícil atribuir uma diferença à alteração de código.
Separe duração total percebida pelo chamador de tempo até o primeiro token. Aqui, mediremos uma chamada sem streaming até a resposta final. O guia de latência e TTFT em produção trata da perspectiva operacional; este roteiro protege mudanças específicas antes da liberação.
Escolha três critérios independentes: resposta válida, consumo dentro do orçamento e duração aceitável. Uma resposta rápida e vazia não satisfaz o contrato. Uma extração correta que reenvia todo o histórico sem necessidade também pode representar regressão de consumo.
Escreva a regra de decisão antes de coletar resultados. Por exemplo: ausência de campos obrigatórios sempre reprova; ultrapassagem do orçamento de entrada exige revisão do prompt; mudança de duração em poucas amostras abre investigação. Esses são critérios de engenharia propostos para o projeto, não limites universais do pytest ou da API.
2. Escolha a camada certa: mock ou chamada real
Um mock é uma substituição controlada da dependência externa. Ele permite devolver um objeto de uso conhecido ou provocar uma exceção sem consumir a API. A documentação do pytest apresenta a fixture monkeypatch para substituir atributos e restaurá-los ao final do teste. Outra opção simples é passar explicitamente a função de chamada e o relógio.
| Camada | O que verifica | O que não demonstra | Quando executar |
|---|---|---|---|
| Unidade sem rede | Extração de uso, duração e aplicação de limites | Velocidade ou qualidade do fornecedor | A cada alteração |
| Integração real | Formato efetivo, autenticação e consumo observado | Capacidade sob muitos usuários | Execução deliberada com orçamento |
| Comparação controlada | Diferença entre versões sob casos equivalentes | Garantia de latência futura | Antes de mudanças relevantes |
Não use uma resposta gravada como prova de que o servidor atual continua rápido. Ela é útil para testar compatibilidade do adaptador, mas o relógio estará medindo a reprodução local. Também não deixe o teste unitário acessar a rede por acidente: a dependência deve ser substituída na referência usada pelo módulo em teste.
Reserve chamadas reais para dúvidas que só elas respondem. Testar dez variações do mesmo cálculo de soma com a API ativa apenas aumenta custo e ruído. Já validar uma alteração na forma de enviar documentos pode justificar um pequeno conjunto real com entradas sintéticas representativas.
3. Implemente uma medição verificável
O exemplo abaixo usa o formato de uso da Responses API da OpenAI: input_tokens, output_tokens e total_tokens. Para outro endpoint, implemente um adaptador próprio. Consulte também a página de integrações da OpenAI ao escolher a conexão da aplicação.
Salve como medicao.py. O relógio padrão é perf_counter, recomendado pelo Python para medir intervalos curtos. Ele inclui espera, portanto a duração engloba rede e processamento observados pelo cliente. Não é uma leitura do tempo interno de GPU.
from collections.abc import Callable
from dataclasses import dataclass
from time import perf_counter
from typing import Protocol
class Uso(Protocol):
input_tokens: int
output_tokens: int
total_tokens: int
class Resposta(Protocol):
usage: Uso | None
@dataclass(frozen=True)
class Medicao:
segundos: float
entrada: int
saida: int
total: int
def medir(
chamar: Callable[[], Resposta],
relogio: Callable[[], float] = perf_counter,
) -> Medicao:
inicio = relogio()
resposta = chamar()
duracao = relogio() - inicio
uso = resposta.usage
if uso is None:
raise ValueError("Resposta sem dados de uso")
return Medicao(
duracao, uso.input_tokens,
uso.output_tokens, uso.total_tokens,
)
Agora salve este teste em test_medicao.py. A sequência do relógio simula um intervalo conhecido; não há espera artificial. O teste demonstra que a função preserva os campos recebidos e calcula corretamente a diferença, independentemente da velocidade da máquina.
from dataclasses import dataclass
from medicao import medir
@dataclass(frozen=True)
class UsoFake:
input_tokens: int = 120
output_tokens: int = 30
total_tokens: int = 150
@dataclass(frozen=True)
class RespostaFake:
usage: UsoFake = UsoFake()
def test_medicao_sem_rede() -> None:
instantes = iter((10.0, 10.5))
resultado = medir(RespostaFake, lambda: next(instantes))
assert resultado.segundos == 0.5
assert resultado.entrada == 120
assert resultado.saida == 30
assert resultado.total == 150
Adicione casos para ausência de uso e exceções da chamada. Não transforme uso ausente em zero: zero sugere consumo comprovadamente inexistente, enquanto ausência significa que você não dispõe da informação. Erros devem continuar visíveis ao teste chamador.
4. Execute a integração com limites explícitos
Instale pytest e o SDK em um ambiente isolado, registre suas versões no projeto e forneça a chave apenas por variável de ambiente. O identificador do modelo também deve vir da configuração, conferido na conta utilizada. Não copie um nome de modelo de um exemplo antigo esperando disponibilidade automática.
Cadastre o marcador em pytest.ini. A opção de seleção permite executar unidades sem chamar a integração. A documentação oficial recomenda registrar marcadores personalizados, inclusive para detectar erros de digitação com marcadores estritos.
[pytest]
markers =
api_real: teste pago com acesso à API
addopts = --strict-markers
Este teste mínimo exige autorização operacional por variável e limites definidos pelo projeto. Os valores dos limites não estão embutidos porque precisam nascer da sua referência medida. A chamada desativa novas tentativas automáticas para observar uma tentativa por caso; a política real de produção deve ser avaliada separadamente.
import os
import pytest
from openai import OpenAI
from medicao import medir
@pytest.mark.api_real
def test_orcamento_api() -> None:
if os.getenv("EXECUTAR_API_REAL") != "1":
pytest.skip("Integração real desabilitada")
with OpenAI(timeout=30.0, max_retries=0) as cliente:
resultado = medir(lambda: cliente.responses.create(
model=os.environ["MODELO_TESTE"],
input="Responda apenas com a palavra: recebido",
max_output_tokens=256,
))
assert resultado.entrada <= int(os.environ["TETO_ENTRADA"])
assert resultado.total <= int(os.environ["TETO_TOTAL"])
assert resultado.segundos <= float(os.environ["TETO_SEGUNDOS"])
Execute unidades com pytest -m "not api_real" e a integração com pytest -m api_real, após configurar as variáveis. O timeout exemplificado é uma configuração de cliente, não uma garantia de prazo total do processo. Uma suíte com várias chamadas também precisa de orçamento global e supervisão do executor.
O exemplo verifica um teto por chamada; ele não constitui teste estatístico de desempenho nem avaliação da resposta textual. Acrescente a verificação funcional da sua tarefa antes de usar a suíte como critério de liberação. Para detalhes de configuração do cliente, consulte o README oficial do SDK listado nas fontes.
5. Compare versões sem criar falsos alarmes
Construa uma referência com execuções anteriores registradas sob condições conhecidas. Preserve duração individual, sucesso, erro, tokens, parâmetros, versão do código e identificação da execução. Uma média isolada impede descobrir se a mudança afetou todos os casos ou apenas um documento longo.
Compare casos equivalentes. Intercale a ordem entre versão atual e candidata quando possível; executar toda a referência pela manhã e a candidata horas depois confunde alteração de código com variação do serviço. Mantenha igual a concorrência do cliente e documente aquecimento, reutilização de conexão e comportamento de cache.
Evite declarar um percentil alto confiável com poucas observações. Uma amostra pequena pode servir como sinal exploratório, mas não descreve bem eventos raros. Para uma rotina barata, uma mediana acompanhada dos valores individuais pode ser mais honesta do que um p99 apresentado sem contexto.
Defina uma tolerância absoluta e outra relativa com base no ruído observado. Um aumento percentual grande sobre uma duração minúscula pode ser irrelevante para o usuário. Inversamente, uma pequena variação relativa em tarefa já lenta pode violar o prazo de negócio. Investigue repetição do padrão antes de atribuir causalidade.
Não compare tokens com caracteres. A documentação da OpenAI explica que o uso de saída inclui tokens gerados que podem não aparecer no texto visível. Consulte os campos retornados e preserve a separação entre entrada e saída. A tradução disso em dinheiro exige preços vigentes e regras aplicáveis; o guia de tokens e custo de API ajuda a organizar essa conta.
Exemplo: central de assistência técnica em Campinas
Considere uma assistência técnica de Campinas que usa uma API para classificar solicitações de reparo. Este cenário é ilustrativo, sem resultados medidos. A equipe quer trocar um prompt extenso por instruções mais curtas, preservando categorias como orçamento, garantia e acompanhamento de serviço.
O responsável monta casos sintéticos em português, incluindo mensagens curtas, relatos ambíguos e entradas com texto desnecessário. Cada caso tem categoria esperada e dados fictícios. Primeiro, as unidades verificam se o adaptador coleta tokens corretamente e se uma exceção não vira resposta aprovada.
Depois, a equipe roda as duas versões com o mesmo conjunto e registra consumo e duração. Se a candidata usa menos entrada, mas erra a categoria de garantia, ela não passa. Se um único caso demora mais por erro de rede, o registro permanece e recebe classificação própria; ele não desaparece da planilha para melhorar a média.
A decisão considera acerto, orçamento e estabilidade observada. O processo completo de testar um agente antes da produção inclui ainda permissões e comportamento das ferramentas. A medição de tokens cobre uma dimensão específica desse conjunto.
Erros comuns
- Usar relógio de calendário: ajustes no relógio do sistema prejudicam a interpretação de intervalos. Prefira um contador apropriado para duração.
- Repetir até passar: tentativas adicionais selecionadas depois da falha distorcem a evidência. Defina o protocolo antes.
- Executar integrações em paralelo sem controle: contenção e limites da conta podem transformar o teste em outro experimento.
- Mudar o limite junto com o prompt: a referência perde valor quando toda regressão recebe um teto maior sem justificativa registrada.
- Guardar prompts reais indiscriminadamente: prefira casos sintéticos e retenha apenas os dados necessários ao diagnóstico.
- Ignorar falhas: timeout e erro de autenticação precisam aparecer separados dos sucessos, nunca como latência zero.
Perguntas frequentes
Pytest mede a latência sozinho?
Ele executa testes e verifica condições. A medição do exemplo é feita pelo Python dentro do código testado. Você define os limites, a coleta e o significado de uma falha.
Todo teste precisa chamar a API?
Não. A maior parte da lógica de instrumentação pode ser validada com respostas controladas. As chamadas reais devem responder perguntas específicas sobre integração e comportamento observado.
Temperatura baixa torna a execução determinística?
Não trate um parâmetro de geração como garantia de repetição exata. Preserve parâmetros e entradas, mas avalie contratos e faixas adequadas ao comportamento observado, em vez de exigir texto idêntico sem necessidade.
Posso bloquear a entrega por uma chamada lenta?
Um prazo máximo funcional pode justificar isso. Para inferir regressão de desempenho, uma única observação costuma ser evidência insuficiente. Separe prazo de segurança da comparação entre distribuições.
O teste deve incluir as novas tentativas?
Depende do contrato medido. Avalie uma tentativa para isolar a integração e o fluxo completo para representar a experiência da aplicação. Identifique claramente cada resultado e contabilize todas as chamadas observáveis.
Fontes e leitura recomendada
- Python: perf_counter e medição de intervalos
- pytest: substituição controlada de dependências
- pytest: marcadores personalizados
- pytest: causas e tratamento de testes instáveis
- OpenAI: interpretação da contagem de tokens
- OpenAI: criação de respostas e campos de uso
- SDK oficial: timeout, tentativas e cliente Python