O metamorphic testing encontrou 147 bugs confirmados no GCC e no LLVM, defeitos em simuladores comerciais de ADAS usados por fabricantes de equipamentos originais (OEMs) automotivos, e uma falha fatal em um sistema de percepção de carro autônomo oito dias antes de ele matar um pedestre. A técnica parece acadêmica, mas os bugs não são.
O problema é o oracle problem. Para muitos programas, você pode executar entradas, mas não pode verificar independentemente se as saídas estão corretas. Qual é o caminho mais curto exato por uma rede rodoviária com 10.000 nodes? Essa otimização do compiler preserva a semântica? A classificação desse modelo de ML está realmente certa? Você não sabe. Os unit tests tradicionais colapsam aqui porque você não pode escrever um assertEquals(expected, actual) quando não tem ideia do que expected deveria ser.
O metamorphic testing contorna isso ao não verificar as saídas de forma alguma. Ele verifica relações entre saídas.
O que é metamorphic testing?
O metamorphic testing é uma técnica em que você transforma uma entrada em uma entrada relacionada, executa ambas no seu programa, e afirma que as duas saídas obedecem a uma relação matemática ou lógica conhecida. A relação é chamada de metamorphic relation.
Se o seu programa calcula a média de uma lista de números, você não precisa saber a média exata de [4.2, 1.7, 9.3, 2.1] para testá-la. Você só precisa saber que embaralhar a lista deve produzir o mesmo resultado, ou que dobrar cada elemento deve dobrar a média. Essas são metamorphic relations.
A primeira entrada é o source test case. A entrada transformada é o follow-up test case. O oráculo é a própria relação.
Aqui está um exemplo concreto em Python:
import random
def compute_average(numbers):
"""Returns the arithmetic mean of a list of numbers."""
if not numbers:
raise ValueError("empty list")
return sum(numbers) / len(numbers)
def test_average_permutation_invariant():
"""MR-1: Shuffling the input should not change the average."""
source = [4.2, 1.7, 9.3, 2.1, 5.6]
follow_up = source.copy()
random.shuffle(follow_up)
source_out = compute_average(source)
follow_up_out = compute_average(follow_up)
assert source_out == follow_up_out, (
f"Permutation MR failed: {source_out} != {follow_up_out}"
)
def test_average_scaling():
"""MR-2: Doubling every element should double the average."""
source = [3.0, 6.0, 9.0]
follow_up = [x * 2 for x in source]
source_out = compute_average(source)
follow_up_out = compute_average(follow_up)
assert follow_up_out == source_out * 2, (
f"Scaling MR failed: {follow_up_out} != {source_out * 2}"
)
def test_average_inclusion():
"""MR-3: Appending the average itself should not decrease the average."""
source = [10.0, 20.0, 30.0]
source_out = compute_average(source)
follow_up = source + [source_out]
follow_up_out = compute_average(follow_up)
assert follow_up_out == source_out, (
f"Inclusion MR failed: {follow_up_out} != {source_out}"
)
if __name__ == "__main__":
test_average_permutation_invariant()
test_average_scaling()
test_average_inclusion()
print("All metamorphic relations passed.")
Se alguma dessas relações falhar, você encontrou um bug sem nunca calcular a média esperada manualmente. Essa é a ideia central.
Bugs reais encontrados em sistemas de produção
A técnica não é teórica. Aqui estão casos documentados em que o metamorphic testing encontrou bugs reais em software de produção.
147 bugs no GCC e no LLVM
Pesquisadores aplicaram o metamorphic testing em pipelines de otimização de compilers C e encontraram 147 bugs confirmados no GCC e no LLVM. Esses não eram programas de brinquedo. Eram bugs reais de compilação incorreta em que um programa C correto, ao ser executado por um compiler otimizador, produzia código de máquina incorreto. Alguns desses bugs existiam há anos. As metamorphic relations eram simples: se você fizer o inline de uma função manualmente, a saída otimizada deve se comportar igual à original. Se você permutar instruções independentes, o resultado não deve mudar. Os desenvolvedores do compiler confirmaram e corrigiram esses bugs.
compilers de shaders Vulkan no Google
A equipe GraphicsFuzz do Google colocou o metamorphic testing randomizado em produção para o Khronos Vulkan Conformance Test Suite. Eles geraram fragment shaders aleatórios, aplicaram transformações que preservam semântica (como envolver expressões em funções de identidade ou adicionar código morto), e compararam imagens renderizadas entre diferentes compilers e GPUs. Quando dois shaders supostamente equivalentes produziam pixels diferentes, eles haviam encontrado um bug no compiler. A equipe construiu um pipeline inteiro chamado gfauto para reduzir, desduplicar e reportar esses casos. Eles encontraram bugs no ecossistema de ferramentas que transformam, otimizam e validam shaders Vulkan, incluindo drivers de produção enviados aos usuários finais.
Simuladores de ADAS usados por OEMs automotivos
Uma equipe testou três plataformas populares de simulação de ADAS — Simulink, CarMaker e 51Sim-One Cloud —, focando nos seus sistemas de assistência à manutenção de faixa. Casos de teste comuns passaram nas três plataformas. Nenhum problema. Então a equipe aplicou metamorphic relations geométricas: espelhar a cena da estrada horizontalmente, rotacionar a posição do veículo, aplicar transformações afins às marcações de faixa. As saídas deveriam se transformar de forma previsível. Não se transformaram. Bugs foram revelados nas três plataformas. A MathWorks e a IPG Automotive confirmaram os problemas posteriormente. Essas são as mesmas plataformas usadas para validar software antes dele ir para os veículos.
O defeito do self-driving car na mesma classe
Em um dos casos mais contundentes, pesquisadores aplicaram o metamorphic testing em um sistema de detecção de objetos para veículos autônomos e encontraram um bug no pipeline de percepção. O sistema falhou em classificar corretamente pedestres sob entradas transformadas específicas. Eles reportaram. O bug que eles encontravam era da mesma classe de defeito que tem sido implicada em colisões fatais de self-driving car com pedestres.
A compensação: as relações são específicas de domínio
O metamorphic testing é poderoso, mas não é gratuito. A parte difícil é identificar boas metamorphic relations. Uma relação ruim lhe dá falsa confiança. Uma relação muito fraca não vai pegar bugs. Uma relação muito forte vai falhar em comportamento correto devido a ruído de ponto flutuante ou não-determinismo.
Projetar relações requer conhecimento de domínio. Para um algoritmo de caminho mais curto, boas relações incluem: o custo do caminho A→B deve ser igual a B→A em um grafo não direcionado; adicionar uma constante a cada peso de aresta deve adicionar essa constante vezes o número de arestas ao custo total do caminho. Para um algoritmo de ordenação: reverter uma lista ordenada e ordená-la deve dar o reverso da lista ordenada original; cada elemento na saída deve aparecer na entrada com a mesma frequência.
Você não pode reutilizar as mesmas relações em sistemas não relacionados. Esse é o custo.
A aritmética de ponto flutuante é outra armadilha. Muitas relações assumem igualdade exata, mas 0.1 + 0.2 != 0.3 no IEEE 754. Você precisa de comparações baseadas em tolerância, e escolher a tolerância certa é um problema por si só. Muito apertada e você obtém falsos positivos. Muito folgada e você perde bugs reais.
Como adicionar o metamorphic testing ao seu codebase
Você não precisa de um framework. Você precisa de disciplina.
Comece com as funções no seu codebase que não têm oráculo. Inferência de ML, algoritmos de otimização, computações geométricas, agregações estatísticas e código de simulação são todos candidatos. Para cada um, pergunte: o que deve ser verdade sobre a saída se eu mudar a entrada de uma forma específica e previsível?
Escreva uma metamorphic relation por função de teste. Nomeie-a claramente. Execute-a em CI junto com seus unit tests. Quando uma relação falhar, trate-a exatamente como qualquer outra falha de teste.
Aqui está um exemplo um pouco mais realista testando uma função de busca de caminho:
import math
def shortest_path_cost(graph, start, end):
"""Returns the cost of the shortest path. Assume implemented."""
pass
def test_shortest_path_undirected_symmetry():
"""MR: In an undirected graph, path cost A->B equals B->A."""
graph = {
'A': [('B', 3.0), ('C', 1.0)],
'B': [('A', 3.0), ('C', 1.0)],
'C': [('A', 1.0), ('B', 1.0)],
}
ab = shortest_path_cost(graph, 'A', 'B')
ba = shortest_path_cost(graph, 'B', 'A')
assert math.isclose(ab, ba, rel_tol=1e-9), f"Symmetry failed: {ab} != {ba}"
def test_shortest_path_subpath():
"""MR: The shortest path cost cannot exceed any specific path's cost."""
graph = {
'A': [('B', 2.0), ('C', 10.0)],
'B': [('C', 2.0)],
'C': [],
}
cost = shortest_path_cost(graph, 'A', 'C')
assert cost <= 10.0, f"Subpath MR failed: {cost} > 10.0"
assert math.isclose(cost, 4.0, rel_tol=1e-9), f"Expected 4.0, got {cost}"
Você não está testando o algoritmo em si. Você está testando sua implementação dele.
Perguntas frequentes
O metamorphic testing substitui unit tests?
Não. Ele os complementa. Use unit tests quando você conhece a saída esperada. Use testes de metamorphic quando não conhece.
Posso usar isso para modelos de ML?
Sim, e é uma das áreas de pesquisa mais ativas. Relações como “rotacionar uma imagem de um gato ainda deve classificá-la como um gato” são metamorphic relations. Pesquisadores encontraram problemas de confiabilidade de modelo e lacunas de equidade usando essa abordagem.
Como sei que minha metamorphic relation está correta?
Você não a prova. Você argumenta a partir da especificação ou das propriedades matemáticas do domínio. Se a sua própria relação estiver com bug, você obterá falsos positivos. Comece com propriedades óbvias e adicione mais conforme ganha confiança.
E quanto a testes flaky?
Sistemas não-determinísticos (algoritmos probabilísticos, código concorrente, sistemas com timeouts) tornam o metamorphic testing mais difícil. Você pode precisar executar múltiplas tentativas ou usar relações estatísticas em vez de igualdade exata.
Comece com uma relação
Você não precisa de um PhD para usar isso. Escolha uma função no seu sistema em que você atualmente pula os testes porque verificar a saída é muito difícil. Escreva uma metamorphic relation. Execute-a. Se quiser ir mais fundo, a ferramenta gfauto da equipe GraphicsFuzz é open source, e o survey da ACM por Segura et al. cataloga relações em dezenas de domínios.
A técnica encontrou 147 bugs de compiler, defeitos confirmados em plataformas de simulação automotiva, e expôs uma classe de falhas de percepção em carros autônomos antes deles chegarem à estrada. Os bugs são reais. A única questão é se você está procurando por eles.