Você precisa testar uma função onde a saída correta é impossível de saber antecipadamente. Um otimizador de rotas. Um classificador de sentimento. Uma simulação de física. Você leu sobre metamorphic testing: encontre relations entre entradas e saídas que devem se manter, depois teste essas relations em vez de valores exatos.
O problema é criar as relations. Você encara a assinatura da sua função e fica em branco.
Então você pergunta a um LLM. Ele dispara dez metamorphic relations em segundos. A maioria parece plausível. Algumas são obviamente erradas. Uma ou duas são genuinamente inteligentes. O LLM é um parceiro de brainstorming útil, mas não é um substituto para conhecimento de domínio. Cole essas relations na sua test suite sem verificar, e você entregará testes que passam em código quebrado ou falham em código correto.
O Que o LLM Realmente Lhe Dá
Quando você pede a um LLM para propor metamorphic relations para uma função, ele está fazendo correspondência de padrões avançada através dos dados de treinamento. Ele viu milhares de implementações de algoritmos de ordenação, bibliotecas geométricas e pipelines de machine learning. Ele reconhece que “adicionar uma constante deve deslocar a média pela mesma constante” é uma propriedade comum, e sugere isso.
Isso não é compreensão. É associação estatística. O LLM não sabe se sua função lida com listas vazias, valores NaN ou overflow de inteiro. Ele propõe a relation porque já viu outras semelhantes antes.
Aqui está como um prompt e resposta típicos se parecem. Você alimenta o LLM com uma função e pede relations:
# Your function under test
def percentile(values: list[float], p: float) -> float:
"""Return the p-th percentile using linear interpolation."""
if not values:
raise ValueError("empty list")
if not 0.0 <= p <= 1.0:
raise ValueError("p must be in [0, 1]")
sorted_vals = sorted(values)
n = len(sorted_vals)
idx = p * (n - 1)
lower = int(idx)
upper = lower + 1
if upper >= n:
return sorted_vals[-1]
frac = idx - lower
return sorted_vals[lower] * (1 - frac) + sorted_vals[upper] * frac
Prompt: Propose 5 metamorphic relations for this percentile function.
Response:
1. Monotonicity: if p1 < p2, then percentile(values, p1) <= percentile(values, p2)
2. Scaling: percentile([v * k for v in values], p) == percentile(values, p) * k
3. Translation: percentile([v + c for v in values], p) == percentile(values, p) + c
4. Permutation invariance: percentile(values, p) == percentile(shuffled(values), p)
5. Boundary: percentile(values, 0.0) == min(values), percentile(values, 1.0) == max(values)
Três dessas estão corretas e úteis. Uma está sutilmente errada. Uma é trivialmente verdadeira, mas tão frouxa que quase não captura bugs.
A relation de monotonicidade está correta, embora você precise lidar com valores duplicados. A relation de escalonamento falha para k <= 0 porque a ordem de ordenação inverte. A relation de translação é sólida. A invariância de permutação testa principalmente se você se lembrou de ordenar. A relation de contorno está correta apenas se sua interpolação trata os percentis 0 e 100 como min e max.
O LLM não o avisa sobre nenhuma dessas coisas. Ele apresenta todas as cinco com igual confiança.
Como Filtrar Relations Geradas por LLM
O fluxo de trabalho útil não é “pergunte ao LLM, copie a saída, vá almoçar”. É “pergunte ao LLM, trate a saída como uma lista de candidatos, depois verifique cada candidato com raciocínio e testes”.
O primeiro passo é classificar cada relation proposta por tipo. Relations estruturais, como invariância de permutação ou idempotência, tendem a ser mais seguras porque dependem menos da semântica de domínio. Relations aritméticas, como escalonamento ou translação, são poderosas quando se mantêm, mas frequentemente falham em edge cases que o LLM não considerou: multiplicadores negativos, coleções vazias, arredondamento de ponto flutuante.
O segundo passo é a caça a contraexemplos. Para cada relation proposta, tente encontrar uma entrada onde ela falha. Essa é a forma mais rápida de identificar alucinações de LLM.
def test_percentile_scaling_counterexample():
"""The LLM proposed scaling. It fails for negative k."""
values = [1.0, 2.0, 3.0, 4.0]
original = percentile(values, 0.5) # 2.5
k = -1.0
scaled_values = [v * k for v in values]
scaled_result = percentile(scaled_values, 0.5) # -2.5
# The relation holds here by accident. For nearest-rank,
# multiplying by a negative flips sort order and breaks it.
assert abs(scaled_result - original * k) < 1e-9
A relation de escalonamento por acaso se mantém para interpolação linear, mas você só sabe disso porque testou. O LLM não sabia. Ele adivinhou com base em correspondência de padrões. Um algoritmo de percentil diferente quebraria o escalonamento de formas óbvias.
O terceiro passo é mutation testing. Uma vez que você tem uma relation como teste, execute uma ferramenta de mutation testing contra ela. Se mutantes sobreviverem, sua relation é muito fraca. Se código correto for morto, sua relation está errada.
Onde os LLMs Brilham e Onde Eles Falham
Os LLMs são genuinamente úteis para descobrir relations em domínios bem conhecidos. Eles sabem que classificadores de imagem devem ser invariantes a espelhamentos horizontais, que algoritmos de ordenação devem ser idempotentes e que multiplicação de matrizes deve distribuir sobre adição. O LLM recupera essas relations canônicas instantaneamente.
Eles são menos úteis em domínios com restrições implícitas que não aparecem nos dados de treinamento. Se você está testando um motor de precificação personalizado com regras de negócio sobre descontos regionais e interações de códigos promocionais, o LLM não faz ideia. Ele proporá relations aritméticas genéricas que ignoram a lógica de negócio, ou pior, sugerirá relations que a contradizem.
Os modos de falha são previsíveis:
Relations excessivamente gerais. O LLM sugere “a saída deve ser positiva” para uma função que retorna uma probabilidade. Isso é uma verificação de sanidade fraca, não uma metamorphic relation. Captura crashes, mas não bugs de lógica.
Relations que assumem continuidade. O LLM propõe que pequenas mudanças de entrada produzam pequenas mudanças de saída. Isso falha para funções de limiar e classificadores discretos.
Relations que ignoram restrições de tipo. O LLM sugere ordenar uma lista de dataclasses por um campo, depois verificar se o campo do primeiro elemento é o mínimo. Ele esquece que alguns campos podem ser opcionais, ou que o operador de comparação pode não estar definido para aquele tipo.
Relations que são matematicamente falsas. Uma vez o LLM sugeriu que a mediana de uma lista concatenada é igual à média das medianas das sublistas. Isso não é verdade. O LLM a apresentou com a mesma confiança que a invariância de permutação.
Um Fluxo de Trabalho Prático
Não peça ao LLM para substituir seu cérebro. Peça a ele para acelerar a parte onde você encara uma página em branco.
Comece escrevendo uma descrição de um parágrafo da sua função, incluindo tipos, restrições e edge cases conhecidos. Quanto mais contexto você der, menos o LLM alucina.
Peça relations categorizadas por tipo: relations de invariância, relations de monotonicidade, relations aditivas e relations estruturais. Esse enquadramento ajuda o LLM a organizar sua correspondência de padrões e produz uma saída mais consistente.
Para cada relation proposta, passe por esta lista de verificação:
- Ela se mantém para entradas vazias?
- Ela se mantém para entradas de elemento único?
- Ela se mantém para valores negativos, zero, NaN ou infinito?
- Ela se mantém quando a entrada já está em ordem ordenada? Ordem reversa?
- Eu consigo escrever um mutation test que mate mutantes apenas se essa relation se mantiver?
Mantenha as relations que sobrevivem às cinco. Descarte o resto, e documente por quê.
Aqui está um modelo para o prompt que usamos internamente:
SYSTEM_PROMPT = """
You are a testing assistant. Given a Python function, propose metamorphic relations.
For each relation:
1. State the relation clearly
2. Identify the type: invariance, monotonicity, additive, or structural
3. List edge cases where it might fail
4. Rate confidence as HIGH, MEDIUM, or LOW
"""
def generate_relations(source_code: str) -> list[dict]:
response = openai.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"Propose relations:\n\n{source_code}"}
],
temperature=0.3,
)
return parse_relation_candidates(response.choices[0].message.content)
Em 0.3, o LLM é menos criativo, mas mais consistente. Para metamorphic relations, consistência vence criatividade. Você quer as relations chatas e corretas, não as inteligentes e erradas.
O Gargalo Real Ainda é Você
Os LLMs podem acelerar a descoberta, mas não podem substituir a verificação. Uma metamorphic relation que você não validou pessoalmente não é um teste. É um palpite vestido de asserção.
A resposta honesta para “um LLM pode descobrir test oracles para mim?” é parcial. Ele pode descobrir candidatos, refrescar sua memória e sugerir edge cases. Ele não pode dizer quais relations estão corretas para sua implementação específica, com suas restrições específicas, em seu domínio específico.
Essa parte ainda exige um humano que entenda o código. O LLM é um parceiro de brainstorming, não um oráculo para os oráculos.
Se você está começando do zero, escolha uma função com um oráculo fraco, peça a um LLM cinco relations, depois passe vinte minutos tentando quebrar cada uma. As relations que sobrevivem são seu conjunto semente. As que quebram te ensinam mais sobre sua função do que o LLM jamais poderia.