DEV Community

Cover image for Testes de Software com IA: Entre a Cobertura Ilusória e a Qualidade Real
Vinicius Cardoso Garcia
Vinicius Cardoso Garcia

Posted on

Testes de Software com IA: Entre a Cobertura Ilusória e a Qualidade Real

A promessa de testes gerados automaticamente por IA é sedutora: cobertura de código elevada em minutos, centenas de casos de teste sem esforço manual. Ferramentas como GitHub Copilot e ChatGPT conseguem propor suítes inteiras para funções TypeScript com poucos prompts [1]. Entretanto, estudos recentes revelam uma divergência preocupante: coverage alto não implica testes efetivos [2]. Este texto explora como a IA está transformando práticas de teste, onde ela falha, e por que mutation testing e property-based testing são essenciais para validar a qualidade real dos testes gerados.

Geração Automática de Testes: O Panorama Atual

LLMs são excepcionalmente bons em replicar padrões conhecidos de testes. Dado código TypeScript, modelos como GPT-4 e Claude produzem rapidamente casos com entradas válidas, inválidas e valores extremos óbvios [3]. Experimentos mostram ganhos de produtividade significativos: tempo de criação de casos de teste reduzido em 40-60% e aumento consistente de cobertura estrutural [4].

Considere uma função de cálculo de desconto:

export function calculateDiscount(price: number, couponCode?: string): number {
  if (price <= 0) throw new Error('Price must be positive');

  let discountPercent = 0;
  if (couponCode === 'SAVE10') discountPercent = 10;
  else if (couponCode === 'SAVE20') discountPercent = 20;

  const discount = price * (discountPercent / 100);
  return price - discount;
}
Enter fullscreen mode Exit fullscreen mode

Um modelo de IA instruído a gerar testes Jest completos tipicamente produz:

import { calculateDiscount } from './discount';

describe('calculateDiscount', () => {
  // Happy path - IA sempre cobre bem
  it('aplica 10% com SAVE10', () => {
    expect(calculateDiscount(100, 'SAVE10')).toBe(90);
  });

  it('aplica 20% com SAVE20', () => {
    expect(calculateDiscount(100, 'SAVE20')).toBe(80);
  });

  it('retorna preço original sem cupom', () => {
    expect(calculateDiscount(100)).toBe(100);
  });

  // Edge cases típicos - IA encontra alguns
  it('trata preço mínimo positivo', () => {
    expect(calculateDiscount(0.01, 'SAVE10')).toBeCloseTo(0.009);
  });

  // Error cases - IA cobre erros óbvios
  it('lança erro para zero', () => {
    expect(() => calculateDiscount(0)).toThrow('Price must be positive');
  });

  it('lança erro para negativo', () => {
    expect(() => calculateDiscount(-10)).toThrow('Price must be positive');
  });
});
Enter fullscreen mode Exit fullscreen mode

Esse padrão é consistente na literatura: cobertura boa de fluxos principais, com alguns casos extremos óbvios [5]. O problema? Estudos experimentais mostram que LLMs frequentemente falham em edge cases "não convencionais": combinações específicas de parâmetros, interações com estado global, problemas de precisão numérica e condições de corrida [2].

O Paradoxo: Coverage Alto, Mutation Score Baixo

A métrica de cobertura (coverage) mede quantas linhas ou branches foram executados durante os testes, mas não mede se os testes realmente detectam defeitos. É possível ter 95% de cobertura com testes que apenas executam código sem verificar comportamento.

Mutation testing resolve essa limitação. A técnica introduz pequenas alterações (mutantes) no código e verifica quantos são "mortos" pelos testes [6]. O mutation score (0 a 1) indica quão sensíveis são os testes a defeitos reais.

Em TypeScript, Stryker é a principal ferramenta. Ela muta expressões e reexecuta testes:

// Código original
export function calculateTotal(price: number, quantity: number): number {
  return price * quantity;
}

// Mutante 1: operador alterado (+ em vez de *)
export function calculateTotal(price: number, quantity: number): number {
  return price + quantity;
}

// Mutante 2: constante alterada
export function calculateTotal(price: number, quantity: number): number {
  return price * (quantity + 1);
}
Enter fullscreen mode Exit fullscreen mode

Se os testes não falham diante desses mutantes, o mutation score cai, revelando que os asserts são fracos mesmo com coverage alto [7].

Revisões sistemáticas mostraram que testes gerados automaticamente conseguem elevar cobertura para 80-90%, mas mutation scores permanecem em 40-60%, bem abaixo de suítes desenhadas manualmente [8]. Mais alarmante: um estudo recente demonstrou que, sem feedback de mutantes, LLMs podem alcançar 100% de cobertura com mutation score tão baixo quanto 4% em alguns benchmarks [2].

Anatomia de um Teste Superficial vs Teste Efetivo

Para entender a diferença, considere esta função de elegibilidade:

function isEligibleForDiscount(age: number, isPremium: boolean): boolean {
  return age >= 60 || isPremium;
}
Enter fullscreen mode Exit fullscreen mode

Teste superficial (100% coverage, mutation score baixo):

test('retorna true para premium', () => {
  expect(isEligibleForDiscount(30, true)).toBe(true);
});
// Coverage: 100% - todas as linhas foram executadas
// Mas: não testa boundary de idade, não testa false cases
Enter fullscreen mode Exit fullscreen mode

Teste efetivo (100% coverage, mutation score alto):

test('retorna false para não-premium abaixo de 60', () => {
  expect(isEligibleForDiscount(59, false)).toBe(false);
});

test('retorna true para exatamente 60 anos', () => {
  expect(isEligibleForDiscount(60, false)).toBe(true);
});

test('retorna true para premium independente de idade', () => {
  expect(isEligibleForDiscount(25, true)).toBe(true);
});
Enter fullscreen mode Exit fullscreen mode

O segundo conjunto mata mutantes como >=> (testando boundary 60) e ||&& (testando comportamento independente). Essa é a diferença entre "executar código" e "verificar comportamento".

Aspecto Code Coverage Mutation Score
O que mede Linhas/branches executados Mutantes mortos vs sobreviventes
Fácil de aumentar Sim, com smoke tests triviais Não, exige asserts fortes
Sensível à superficialidade Pouco Muito
Risco principal False sense of security Custo computacional maior

Configurar Stryker em um projeto TypeScript é direto:

// stryker.conf.json
{
  "mutate": ["src/**/*.ts", "!src/**/*.test.ts"],
  "testRunner": "vitest",
  "reporters": ["html", "clear-text", "progress"],
  "coverageAnalysis": "perTest",
  "thresholds": { "high": 80, "low": 60, "break": 50 }
}
Enter fullscreen mode Exit fullscreen mode

Property-Based Testing: Além dos Exemplos

Enquanto testes tradicionais verificam exemplos específicos, property-based testing (PBT) testa propriedades gerais do sistema, gerando automaticamente centenas ou milhares de inputs para quebrar invariantes [9]. No ecossistema TypeScript, fast-check é a biblioteca principal, com integração nativa para Vitest.

A ideia central é definir propriedades como "para qualquer preço positivo, o desconto nunca excede o preço original" e deixar o framework procurar contraexemplos:

import { test, expect } from 'vitest';
import fc from 'fast-check';
import { calculateDiscount } from './discount';

test('desconto nunca excede preço original', () => {
  fc.assert(
    fc.property(
      fc.float({ min: 0.01, max: 10_000 }),
      fc.option(fc.constantFrom('SAVE10', 'SAVE20', 'INVALID')),
      (price, coupon) => {
        const result = calculateDiscount(price, coupon ?? undefined);
        expect(result).toBeGreaterThan(0);
        expect(result).toBeLessThanOrEqual(price);
      }
    )
  );
});

test('desconto é idempotente para mesmo input', () => {
  fc.assert(
    fc.property(
      fc.float({ min: 0.01, max: 1000 }),
      fc.constantFrom('SAVE10', 'SAVE20', undefined),
      (price, coupon) => {
        const first = calculateDiscount(price, coupon);
        const second = calculateDiscount(price, coupon);
        expect(first).toBe(second);
      }
    )
  );
});
Enter fullscreen mode Exit fullscreen mode

Propriedades Comuns em Domínios de Software

PBT é muito poderoso justamente quando você identifica invariantes do domínio:

// Propriedade de roundtrip (serialização)
test('JSON roundtrip preserva dados', () => {
  fc.assert(
    fc.property(fc.object(), (obj) => {
      const serialized = JSON.stringify(obj);
      const deserialized = JSON.parse(serialized);
      expect(deserialized).toEqual(obj);
    })
  );
});

// Propriedade de ordenação
test('sort é idempotente', () => {
  fc.assert(
    fc.property(fc.array(fc.integer()), (arr) => {
      const sorted = [...arr].sort((a, b) => a - b);
      const sortedTwice = [...sorted].sort((a, b) => a - b);
      expect(sortedTwice).toEqual(sorted);
    })
  );
});

// Propriedade de transação financeira
test('soma de transações preserva balanço total', () => {
  fc.assert(
    fc.property(
      fc.array(fc.record({ from: fc.string(), to: fc.string(), amount: fc.nat() })),
      (transactions) => {
        const totalBefore = calculateSystemBalance(initialState);
        const totalAfter = calculateSystemBalance(applyTransactions(transactions));
        expect(totalAfter).toBe(totalBefore); // Dinheiro não é criado nem destruído
      }
    )
  );
});
Enter fullscreen mode Exit fullscreen mode

PBT frequentemente revela edge cases que nem desenvolvedores nem IA teriam lembrado como, por exemplo, valores extremos de ponto flutuante, strings Unicode problemáticas, combinações inesperadas de parâmetros [10]. IA pode ajudar a formular propriedades a partir de descrições em linguagem natural, transformando "garantir que soma seja associativa" em código testável.

O Limite da Automação: Julgamento Humano Insubstituível

Em pipelines de CI/CD maduros, o fluxo típico combina várias técnicas:

# .github/workflows/test.yml
name: Quality Gates
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm ci

      # Testes unitários + cobertura
      - run: npm run test:coverage
        env:
          COVERAGE_THRESHOLD: 80

      # Mutation testing (em PRs críticos)
      - run: npm run test:mutation
        if: contains(github.event.pull_request.labels.*.name, 'critical')

      # Property-based testing
      - run: npm run test:properties
Enter fullscreen mode Exit fullscreen mode

Entretanto, estudos empíricos reforçam que o papel do humano desloca-se de "escrever cada linha de teste" para "curar e auditar o conjunto de testes" [4]. Isso inclui: questionar se edge cases de negócio estão cobertos, monitorar métricas além de coverage, e revisar testes gerados para remover redundância.

A metodologia Sinfonia captura essa tensão no Canvas de Testes e Validação: "a natureza probabilística dos modelos generativos significa que as saídas podem variar, tornando os testes de passa/falha insuficientes" [11]. A avaliação precisa ir além da verificação binária para medir qualidade geral com critérios como relevância, veracidade e completude.

Um teste gerado por IA pode verificar que função de pagamento retorna valores corretos. Mas um QA humano questiona: "O que acontece se o usuário tentar pagar com valor 0.00? E se a sessão expirar durante o checkout? Como isso afeta compliance regulatório?" Essas perguntas emergem de empatia com o usuário e conhecimento de domínio, não de análise sintática de código.

A automação inteligente de testes é ferramenta poderosa que amplifica capacidade humana. O futuro pertence às equipes que combinam velocidade da IA com julgamento crítico, usando mutation testing e PBT como "testes dos testes" para garantir que dashboards verdes reflitam qualidade real, não apenas execução superficial.

Referências

[1] Aufiero Informatica (2024). "How Automatic AI Test Case Generation is Revolutionizing Software Testing."

[2] Wang, G. et al. (2025) “Mutation-Guided Unit Test Generation with a Large Language Model,” arXiv:2504.20357.

[3] Codoid (2024). "AI-Generated Test Cases: How Good Are They?"

[4] ThoughtWorks (2024). "AI-Generated Test Cases from User Stories: An Experimental Research Study."

[5] Wang, J. et al. (2024). "Software Testing with Large Language Models: Survey, Landscape, and Vision." IEEE Transactions on Software Engineering, 50(4), pp. 911–936.

[6] Stryker Mutator (2024). "TypeScript Coverage Analysis Support."

[7] Stryker Mutator (2024). "Introduction to Mutation Testing."

[8] Wang, S. et al. (2021) “Automatic Unit Test Generation for Machine Learning Libraries: How Far Are We?,” in 2021 IEEE/ACM 43rd International Conference on Software Engineering (ICSE). IEEE, pp. 1548–1560.

[9] fast-check (2024). "@fast-check/vitest - Property-Based Testing for Vitest."

[10] GitHub Vitest Discussions (2024). "Property-Based Testing Integration."

[11] Garcia, V. C. and Medeiros, R. P. Sinfonia: Orquestrando a Inteligência Artificial. 2025.

Top comments (0)