Tipos de testes de software e o papel da IA

testes de software Types of Software Testing

Uma suíte de testes pode estar verde e, ainda assim, um pull request introduzir um problema sério. Isso acontece porque nenhum tipo de teste consegue cobrir sozinho todos os riscos de uma mudança.

  • Testes unitários validam regras isoladas.
  • Testes de integração verificam se diferentes partes do sistema continuam se comunicando corretamente.
  • Testes funcionais protegem fluxos reais do produto.
  • Já testes de segurança e performance ajudam a encontrar problemas que dificilmente aparecem durante o desenvolvimento local.

Por isso, eu não olharia apenas para o resultado da suíte. Antes de perguntar se os testes passaram, tentaria entender que tipo de risco a mudança criou e qual validação tem mais chance de encontrá-lo.

Essa preocupação ficou ainda mais importante com a IA entrando no desenvolvimento de software. Copilots e agentes aceleram a escrita de código e testes, enquanto ferramentas de AI code review analisam os diffs antes do merge. O volume de mudanças cresce, mas o tempo disponível para entender cada uma delas nem sempre acompanha esse ritmo.

Neste artigo, vou passar pelos principais tipos de testes de software, explicar onde cada um ajuda, mostrar o que a automação não consegue validar sozinha e discutir como testes, CI, code review e IA podem trabalhar juntos antes do merge.

O que são testes de software?

Testes de software verificam se o sistema continua se comportando como esperado depois de uma mudança. Eles ajudam a encontrar falhas, regressões e outros riscos antes que o código chegue à produção.

Existem diferentes tipos de teste porque cada um observa uma parte do sistema. Alguns validam regras isoladas, enquanto outros verificam integrações, fluxos completos, segurança ou performance.

Na prática, escolher o teste certo é uma decisão de risco. O time precisa entender o que pode dar errado, qual seria o impacto e qual tipo de validação tem mais chance de encontrar o problema.

Como escolher o tipo de teste certo

Conhecer os diferentes tipos de testes de software é importante, mas essa classificação, sozinha, não resolve o problema. No dia a dia, o time não precisa apenas saber a diferença entre teste unitário, de integração ou end-to-end. Ele precisa decidir qual combinação de validações faz sentido para a mudança que está sendo feita.

Eu começaria olhando para o risco introduzido pelo pull request.

Se a alteração está concentrada em uma regra isolada, testes unitários podem ser suficientes. Quando ela muda a forma como componentes, serviços ou dependências se comunicam, testes de integração passam a ser mais relevantes. Mudanças em fluxos críticos do produto podem exigir uma validação funcional mais ampla. Já alterações em autenticação, permissões, dados sensíveis ou áreas de alto impacto dificilmente deveriam depender de uma única camada de teste.

O tamanho da mudança também não determina sozinho o esforço necessário. Um diff pequeno pode alterar uma regra crítica, enquanto um PR maior pode envolver apenas uma reorganização interna protegida por uma boa suíte de regressão. Por isso, eu evitaria escolher o teste com base apenas na quantidade de código modificada.

A pergunta que orienta essa decisão é: o que pode dar errado com essa mudança, qual seria o impacto e qual tipo de validação tem mais chance de encontrar o problema antes da produção?

A partir daí, o time consegue distribuir melhor o esforço entre testes automatizados, CI, revisão de código e validações específicas, sem tentar cobrir todo risco da mesma forma.

Principais tipos de testes de software

Os principais tipos de testes de software cobrem riscos diferentes e ajudam o time a validar partes específicas do sistema.

Um time não precisa usar todos com a mesma intensidade. A combinação depende da arquitetura, do produto, da frequência de mudanças e, principalmente, do impacto que uma falha teria em produção.

Testes unitários

Testes unitários verificam pequenas partes do código de forma isolada, como funções, métodos, classes ou componentes.

Um exemplo simples é validar uma função que calcula desconto:

function calculateDiscount(price, percentage) {
  return price - price * (percentage / 100);
}

test('aplica 10% de desconto', () => {
  expect(calculateDiscount(100, 10)).toBe(90);
});

Esse teste verifica uma regra específica sem depender de banco de dados, API, fila ou qualquer outra parte do sistema. Por isso, ele é rápido de executar e simples de interpretar quando falha.

Testes unitários deixam de ser suficientes quando o comportamento depende da interação com outras partes do sistema. Essas dependências podem ser simuladas com mocks, mas, quando quase tudo é substituído, o teste passa a validar mais a simulação do que o funcionamento real da aplicação.

Nesse cenário, o teste pode continuar passando mesmo que a integração entre os componentes esteja quebrada. Ele mostra que a unidade funciona dentro das condições simuladas, mas não garante que o sistema completo continua funcionando corretamente.

Testes de integração

Testes de integração verificam se diferentes partes do sistema continuam funcionando corretamente em conjunto.

Eles ajudam a encontrar problemas que dificilmente aparecem em testes isolados, como contratos incompatíveis, schemas incorretos, migrations que afetam consultas, erros de serialização, configurações ausentes ou falhas na comunicação com serviços externos.

Esses testes costumam exigir mais infraestrutura e levar mais tempo para rodar do que testes unitários, mas são importantes sempre que o comportamento depende da interação entre módulos, bancos, filas ou serviços.

Um exemplo simples é uma alteração no formato de resposta de uma API interna:

// Serviço de pagamentos
return {
  paymentId: payment.id,
  status: payment.status,
};

Se outro serviço ainda espera receber a propriedade id, os testes unitários dos dois lados podem continuar passando caso usem mocks desatualizados. A integração, porém, já está quebrada.

Um teste de integração que chama o serviço real e valida o formato da resposta consegue encontrar essa incompatibilidade antes que ela chegue à produção.

Testes funcionais

Testes funcionais verificam se o software entrega o comportamento esperado a partir de uma regra de negócio ou de um requisito do produto.

O foco está no resultado que o usuário ou outro sistema consegue observar, e não em como o código foi implementado. Em vez de testar uma função isolada, esse tipo de teste valida se um fluxo importante continua funcionando, como criar uma conta, aprovar um pagamento ou cancelar uma assinatura.

Eles podem ser executados manualmente ou automatizados. Quando percorrem a interface e envolvem vários componentes do sistema, podem assumir a forma de testes end-to-end.

import { test, expect } from "@playwright/test";

test("permite finalizar uma compra aprovada", async ({ page }) => {
  await page.goto("/checkout");

  await page.fill('[name="cardNumber"]', "4242424242424242");
  await page.click('button[type="submit"]');

  await expect(page.getByText("Pagamento aprovado")).toBeVisible();
});

Nesse exemplo, o teste não verifica como cada função foi implementada. Ele valida o comportamento que importa para quem usa o produto: depois de enviar um pagamento válido, a compra deve ser aprovada.

Eu evitaria levar todo comportamento para esse nível. Quanto mais partes participam da execução, maior tende a ser o custo para rodar o teste, mantê-lo estável e descobrir a causa de uma falha.

Por isso, eu reservaria esse tipo de teste para fluxos críticos do produto. Regras menores ou comportamentos mais isolados costumam ser protegidos de forma mais eficiente por testes unitários ou de integração.

Testes de regressão

Testes de regressão verificam se uma nova mudança quebrou algo que já funcionava.

Eles não representam uma técnica específica. Uma suíte de regressão pode reunir testes unitários, de integração, funcionais e end-to-end. O que une esses testes é o objetivo de proteger comportamentos existentes e evitar que problemas conhecidos voltem a aparecer.

Um exemplo simples é um bug em que cupons expirados continuavam sendo aplicados no checkout. Depois de corrigir o problema, o time pode adicionar um teste específico para garantir que esse comportamento não volte:

import { describe, expect, it } from "vitest";

describe("applyCoupon", () => {
  it("não aplica cupons expirados", () => {
    const coupon = {
      discount: 20,
      expiresAt: new Date("2026-07-01"),
    };

    const total = applyCoupon(100, coupon, new Date("2026-07-29"));

    expect(total).toBe(100);
  });
});

Esse teste passa a fazer parte da suíte de regressão porque protege um comportamento que já falhou no passado.

Esse é um dos casos em que a automação costuma trazer mais retorno. Quando o mesmo fluxo precisa ser validado depois de cada alteração, depender de uma pessoa para repetir esse trabalho aumenta o tempo da entrega e a chance de inconsistência.

Ao mesmo tempo, uma suíte grande, lenta e instável pode perder utilidade. Quando falhas falsas se tornam frequentes, o time começa a reexecutar jobs até que passem ou deixa de confiar no CI.

Por isso, eu prefiro uma suíte menor, confiável e ligada aos riscos reais do produto a uma quantidade enorme de testes que ninguém consegue interpretar quando falham.

Testes de performance

Testes de performance avaliam como o sistema se comporta sob carga, volume, concorrência ou uso prolongado.

Eles ajudam o time a entender se um endpoint suporta o tráfego esperado, se uma consulta continua eficiente conforme a base cresce ou se uma fila consegue processar mensagens no mesmo ritmo em que elas chegam.

Esse tipo de problema costuma passar despercebido durante o desenvolvimento porque muitos gargalos não aparecem em ambiente local. Uma consulta sem índice, várias chamadas ao banco dentro de um loop ou um processamento pesado executado de forma síncrona podem parecer aceitáveis com poucos dados e causar lentidão quando chegam à produção.

Os testes de performance tentam reproduzir condições mais próximas do uso real para medir tempo de resposta, consumo de recursos, capacidade e estabilidade.

Um exemplo simples com k6 seria simular vários usuários acessando o mesmo endpoint ao mesmo tempo:

import http from "k6/http";
import { check, sleep } from "k6";

export const options = {
  vus: 50,
  duration: "30s",
};

export default function () {
  const response = http.get("https://api.exemplo.com/orders");

  check(response, {
    "retorna status 200": (res) => res.status === 200,
    "responde em menos de 500 ms": (res) =>
      res.timings.duration < 500,
  });

  sleep(1);
}

Esse teste mantém 50 usuários virtuais ativos durante 30 segundos e verifica se o endpoint continua respondendo corretamente e dentro do tempo esperado.

O code review entra como uma camada complementar. Ele pode identificar padrões que merecem atenção antes mesmo de a mudança chegar aos testes, como queries executadas repetidamente, uso inadequado de cache ou processamento pesado em caminhos críticos.

Testes de segurança

Testes de segurança ajudam a identificar vulnerabilidades, configurações incorretas e caminhos que podem ser explorados antes que a mudança chegue à produção.

Essa validação pode envolver análise estática, análise dinâmica, verificação de dependências, pentest, fuzzing e revisão manual de partes sensíveis do sistema. A escolha depende do tipo de risco envolvido e da área alterada pelo pull request.

Muitos problemas de segurança não aparecem como uma falha evidente. Uma checagem de autorização ausente, uma validação feita apenas no cliente, um dado sensível registrado em logs ou uma permissão ampla demais podem passar por testes funcionais e ainda representar um risco sério.

Um exemplo simples é validar se um usuário consegue acessar apenas os recursos da própria organização:

import { describe, expect, it } from "vitest";

describe("GET /accounts/:id", () => {
  it("bloqueia o acesso a contas de outra organização", async () => {
    const response = await request(app)
      .get("/accounts/account-456")
      .set("Authorization", "Bearer token-do-usuario-sem-acesso");

    expect(response.status).toBe(403);
  });
});

Esse teste protege uma regra específica de autorização. Ele não substitui outras validações de segurança, mas ajuda a garantir que uma mudança no endpoint não exponha dados de outra organização.

Ferramentas especializadas continuam sendo fundamentais. Aqui a revisão de código com IA pode funcionar como uma camada complementar, principalmente quando o diff altera autenticação, autorização, criptografia, dados pessoais ou a comunicação entre serviços.

Testes manuais e exploratórios

Testes manuais continuam sendo importantes quando o time precisa avaliar usabilidade, investigar comportamentos inesperados ou validar partes novas do produto.

Uma pessoa explorando o sistema consegue perceber problemas que uma automação não foi criada para encontrar. Isso é especialmente útil quando o comportamento esperado ainda está sendo definido ou quando a avaliação depende de contexto e julgamento.

O problema começa quando o teste manual vira o caminho padrão para validar tarefas repetitivas e previsíveis. Nesse cenário, o processo fica mais lento, inconsistente e dependente da atenção de quem executa.

Fluxos críticos, regressões conhecidas e regras estáveis deveriam ser automatizados sempre que o custo fizer sentido. O trabalho manual gera mais valor quando está voltado para investigação e descoberta, e não para repetir o mesmo roteiro antes de cada release.

Qual teste usar em cada mudança?

Uma forma prática de escolher o teste certo é relacionar cada mudança ao risco que ela introduz.

  • Mudanças em regras pequenas e isoladas normalmente pedem testes unitários.
  • Alterações em banco, filas, APIs e contratos entre módulos pedem testes de integração.
  • Mudanças em fluxos críticos do usuário podem exigir testes funcionais ou end-to-end.
  • Bugs corrigidos devem se transformar em casos de regressão sempre que possível.
  • Mudanças em autenticação, permissões ou dados sensíveis precisam de validações de segurança.
  • Alterações em consultas, processamento em lote ou caminhos de alto volume podem exigir testes de performance.

Essa análise evita dois extremos.

O primeiro é depender quase exclusivamente de testes unitários e descobrir tarde que os componentes não funcionam juntos. O segundo é testar tudo no nível mais alto e criar uma suíte lenta, cara e difícil de manter.

Na maioria dos sistemas, o melhor desenho combina muitos testes rápidos com um número menor de testes mais caros nos fluxos em que uma falha teria maior impacto.

O que os testes automatizados não conseguem validar sozinhos

Mesmo uma boa suíte de testes automatizados deixa pontos cegos.

Os testes validam cenários e expectativas que alguém decidiu escrever. Eles não entendem, por conta própria, a intenção do pull request, o histórico de incidentes do time ou as regras arquiteturais que orientam o projeto.

Por isso, alguns problemas ainda podem passar mesmo quando toda a suíte está verde:

  • uma mudança que viola uma convenção importante do time;
  • uma função sem cobertura para o caso limite mais arriscado;
  • uma autorização aplicada em um endpoint e esquecida em outro;
  • uma dependência indevida entre camadas;
  • um teste que protege um comportamento incorreto;
  • uma implementação que funciona, mas torna o sistema mais difícil de manter;
  • um PR grande demais para ser revisado com atenção.

É nesse espaço que o code review continua sendo necessário.

O teste automatizado responde se uma expectativa declarada foi atendida. O review ajuda a entender se essa expectativa está correta, se a mudança faz sentido no contexto do sistema e se algum risco relevante ficou sem validação.

Como a IA pode ajudar nos testes de software

A IA pode ajudar quando reduz trabalho repetitivo e direciona a atenção do time para riscos que realmente importam.

O problema aparece quando ela gera testes genéricos, comentários óbvios ou sugestões que não consideram o contexto do projeto. Nesse caso, em vez de melhorar a revisão, ela apenas adiciona mais ruído ao processo.

Para mim, a diferença está no contexto disponível. Uma IA olhando apenas para uma função isolada consegue sugerir casos básicos. Já uma ferramenta que entende o diff, o repositório, as regras do time e os arquivos relacionados tem mais chance de identificar uma lacuna relevante antes do merge.

Sugestão de testes durante o pull request

Dentro de um pull request, a IA pode analisar o código alterado e identificar cenários que ainda precisam de validação.

Se uma função ganhou uma nova condição, ela pode perceber que um dos caminhos ficou sem cobertura. Se um endpoint passou a aceitar um novo campo, pode levantar casos de validação que ainda não foram considerados. E, quando uma migration altera uma coluna usada em outros módulos, pode indicar quais integrações merecem uma revisão mais cuidadosa.

O valor está em ajudar o time a perceber lacunas enquanto a mudança ainda está em revisão. A decisão sobre quais testes realmente fazem sentido continua sendo de quem conhece o comportamento esperado e o impacto daquela alteração.

Geração de casos de teste

Ferramentas com IA também podem sugerir casos de teste com base no código, nos contratos de API e nos padrões existentes no repositório.

Isso pode acelerar bastante a escrita, principalmente quando o comportamento já está claro e o trabalho restante é transformar os cenários em código.

Ainda assim, eu revisaria testes gerados por IA com o mesmo cuidado usado para revisar código de produção.

Um teste pode parecer correto e não proteger nada relevante. Também pode ficar acoplado demais à implementação, repetir a lógica testada ou ignorar justamente o comportamento que deveria garantir.

Gerar um teste é fácil. Garantir que ele falhe pelo motivo certo e proteja o comportamento esperado é outra coisa.

Identificação de casos limite

A IA também pode ajudar a levantar perguntas que seriam fáceis de esquecer durante uma revisão rápida.

Uma mudança em cálculo financeiro, permissões ou tratamento de erros pode exigir casos como:

  • valores nulos ou negativos;
  • limites máximos e mínimos;
  • usuário sem acesso;
  • falha de uma API externa;
  • dados incompletos;
  • chamadas concorrentes;
  • repetição da mesma operação;
  • rollback depois de uma falha parcial.

Essas perguntas não são novas. O valor está em fazê-las aparecer no momento em que a mudança ainda está sendo revisada, sem depender apenas de uma pessoa.

Aplicação das regras do time

Algumas regras importantes não cabem bem em uma suíte de testes tradicional.

Por exemplo:

  • a camada de domínio não pode importar infraestrutura;
  • todo endpoint novo precisa validar autorização;
  • mudanças em cobrança precisam incluir testes de regressão;
  • nenhum dado sensível pode aparecer em logs;
  • determinados módulos não podem acessar diretamente o banco;
  • código de produção não deve conter chamadas de debug.

Ferramentas de AI code review podem verificar essas regras diretamente no diff.

Na Kodus, por exemplo, o time pode registrar regras de revisão em linguagem natural e aplicá-las aos pull requests. A ideia não é substituir o CI ou a revisão humana, mas tornar verificações recorrentes mais consistentes antes do merge.

Como combinar testes, CI e code review

Testes, CI e code review não precisam fazer a mesma coisa. Cada etapa ajuda a encontrar um tipo de problema, e o fluxo funciona melhor quando essa divisão está clara.

Cada parte do processo tem uma responsabilidade diferente:

  • testes unitários validam regras pequenas rapidamente;
  • testes de integração verificam a comunicação entre componentes;
  • testes funcionais protegem fluxos importantes do produto;
  • testes de regressão impedem que problemas conhecidos voltem;
  • testes de segurança e performance cobrem riscos específicos;
  • o CI executa verificações reproduzíveis antes do merge;
  • o code review avalia intenção, impacto, arquitetura e aderência às regras do time;
  • a IA ajuda a encontrar lacunas e aplicar critérios de forma consistente.

O objetivo é evitar que o pull request chegue à revisão cheio de problemas que poderiam ter sido encontrados automaticamente.

O CI deve bloquear verificações objetivas. Os testes devem proteger comportamentos importantes. A revisão humana deve se concentrar nas decisões que exigem contexto. E a IA pode ajudar a conectar essas etapas, principalmente quando conhece as regras e a arquitetura do projeto.

Testes de software começam pelo risco da mudança

Conhecer os tipos de testes de software é importante, mas decorar as categorias não ajuda muito se o time não consegue relacioná-las aos riscos do produto.

Antes de decidir o que testar, eu tentaria responder:

O que pode quebrar com esta mudança, qual seria o impacto e qual camada tem mais chance de encontrar o problema antes da produção?

Alguns riscos serão cobertos por testes automatizados. Outros exigirão análise de segurança, validação de performance ou uma revisão cuidadosa do diff.

A IA pode ajudar nesse processo ao sugerir cenários, encontrar lacunas e verificar regras do time dentro do pull request. Mas ela não substitui uma estratégia de testes nem o julgamento de quem entende o sistema.

A Kodus leva essa análise para o fluxo de code review. Ela considera o contexto do repositório, permite criar regras personalizadas e funciona com GitHub, GitLab, Bitbucket e Azure DevOps.

Para times que querem adotar AI code review com mais controle sobre modelo, custo e regras, a Kodus é uma plataforma de AI code review open source agnóstica a modelos. O objetivo é dar mais contexto para a revisão e trazer riscos importantes para a discussão antes do merge.