Seu teste de webhook passa uma vez. Mesmo assim, o handler pode aceitar um corpo alterado, processar o mesmo evento duas vezes ou esconder uma falha que fará o provedor tentar de novo.
O teste útil começa na fronteira da entrega. Ele envia os bytes exatos que foram assinados, verifica o cabeçalho, valida o evento, confirma a resposta HTTP e mede o efeito produzido. Depois repete o evento, força erros e muda a ordem das entregas.
Este é um caso diferente de escolher o tipo de teste de software. Aqui a pergunta não é se um teste é unitário ou de integração. É qual comportamento do webhook cada cenário consegue provar. O código de exemplo é ilustrativo e não foi executado contra um provedor real.

Resposta curta
- Preserve o corpo bruto antes de fazer
JSON.parsee assine o mesmo conjunto de bytes que o provedor assina.- Teste assinatura ausente, inválida e expirada, além do caminho válido.
- Envie o mesmo evento duas vezes, inclusive em paralelo, e confirme um único efeito de negócio.
- Simule timeout, resposta
5xx, reentrega e eventos fora de ordem sem esperar o relógio real.
O que um teste de webhook precisa provar?
Um teste de webhook precisa responder cinco perguntas: a requisição é autêntica, o payload tem o formato esperado, o endpoint reconhece a entrega, a repetição é segura e o efeito correto acontece uma vez. Um 200 isolado responde apenas uma parte da terceira pergunta.
A especificação do Standard Webhooks, publicada pelo projeto Standard Webhooks, trata assinatura, timestamp e identificador de entrega como metadados distintos do corpo. O identificador pode funcionar como chave de idempotência, enquanto o timestamp ajuda a limitar replay. O seu provedor pode usar nomes e fórmulas diferentes, então o contrato dele continua sendo a referência.
| Pergunta | Cenário mínimo | Evidência a guardar |
|---|---|---|
| A origem foi autenticada? | assinatura válida e inválida | status e motivo da rejeição |
| O corpo foi interpretado corretamente? | JSON válido, inválido e evento desconhecido | erro de validação ou tipo aceito |
| O provedor pode parar de reenviar? | sucesso, 4xx e 5xx |
resposta e estado da entrega |
| Uma duplicata é segura? | mesmo ID duas vezes | um efeito, duas respostas aceitáveis |
| A ordem importa? | eventos relacionados em ordens diferentes | estado final ou reconciliação explícita |
Essa matriz transforma uma integração em um contrato revisável. Também impede que a suíte use a palavra "webhook" para esconder uma chamada direta a uma função interna. O teste deve atravessar a mesma fronteira que o provedor atravessará.
O resultado mais informativo não é o passed isolado. Ele combina resposta, evento aceito, ID de entrega, estado persistido e efeitos externos. Quando uma duplicata aparece, essa trilha mostra se o handler recusou a entrega cedo ou se executou o trabalho antes de descobrir que ela já existia.
Como montar uma fixture determinística?
Monte a fixture como uma requisição HTTP completa, não como um objeto JavaScript que pula a etapa de serialização. O corpo bruto, os cabeçalhos, o segredo de teste, o ID do evento e o timestamp precisam ser controláveis. Assim, a suíte consegue repetir exatamente a mesma entrega.
O Node.js, em "Crypto", fornece createHmac para calcular um HMAC. O algoritmo e o texto assinado abaixo são apenas um contrato genérico. Stripe, GitHub e outros provedores definem seus próprios cabeçalhos, prefixos e regras de timestamp. Copie a fórmula do provedor, não este exemplo por hábito.
import { createHmac } from "node:crypto";
type WebhookRequest = {
body: string;
headers: Record<string, string>;
};
export function makeWebhookRequest(
body: string,
secret: string,
eventId = "evt_test_123",
timestamp = 1_758_000_000,
): WebhookRequest {
const signedValue = `${timestamp}.${body}`;
const signature = createHmac("sha256", secret)
.update(signedValue)
.digest("hex");
return {
body,
headers: {
"content-type": "application/json",
"x-webhook-id": eventId,
"x-webhook-timestamp": String(timestamp),
"x-webhook-signature": signature,
},
};
}
Use uma string fixa para o corpo durante o teste. Se o handler fizer parse e depois serializar o JSON antes da verificação, uma diferença de espaços, ordem de propriedades ou escape pode invalidar uma assinatura correta. O segredo fica na fixture, nunca no texto de erro ou no log do teste.
Se a aplicação usa um framework que já consumiu o corpo, crie um teste que prove essa configuração. A verificação precisa receber os bytes originais. A documentação do Stripe, em "Receive Stripe events in your webhook endpoint", também alerta que a assinatura deve ser verificada com o corpo recebido e que cada endpoint tem seu próprio segredo.
Quais casos negativos devem falhar?
Comece pelos casos que um atacante ou uma falha de transporte pode produzir: cabeçalho ausente, segredo errado, corpo modificado depois da assinatura, timestamp fora da janela e JSON inválido. O teste de caminho feliz só é útil depois que esses limites têm uma resposta definida.
O Stripe documenta timestamp e assinatura como parte da proteção contra replay em "Receive Stripe events in your webhook endpoint". A mesma documentação explica que uma nova tentativa pode receber uma assinatura e um timestamp novos. Isso significa que a suíte deve distinguir um retry legítimo do mesmo evento de um replay antigo ou de uma requisição forjada.
import { describe, expect, it } from "vitest";
describe("webhook verification", () => {
it("accepts the exact signed body", async () => {
const request = makeWebhookRequest(
'{"type":"order.created","id":"evt_test_123"}',
"test-secret",
);
const result = await handleWebhook(request, "test-secret");
expect(result.status).toBe(202);
});
it.each([
["missing signature", (request: WebhookRequest) => {
delete request.headers["x-webhook-signature"];
}],
["changed body", (request: WebhookRequest) => {
request.body = '{"type":"order.cancelled","id":"evt_test_123"}';
}],
["wrong secret", (request: WebhookRequest) => {
request.headers["x-webhook-signature"] = "not-the-signature";
}],
])("rejects %s", async (_name, change) => {
const request = makeWebhookRequest(
'{"type":"order.created","id":"evt_test_123"}',
"test-secret",
);
change(request);
const result = await handleWebhook(request, "test-secret");
expect(result.status).toBe(401);
});
});
O trecho chama handleWebhook como uma fronteira do sistema e é deliberadamente ilustrativo. A suíte real deve usar o adapter HTTP da aplicação, o verificador escolhido pelo provedor e uma resposta que não revele o segredo. Também vale testar credencial rotacionada, content type inesperado, campos obrigatórios ausentes e evento desconhecido.
Como provar que duplicatas não repetem o efeito?
Envie o mesmo evento com o mesmo identificador duas vezes e confirme que o primeiro processamento cria o efeito esperado, enquanto o segundo retorna uma resposta segura sem executar a lógica de negócio novamente. Depois faça duas chamadas simultâneas. A segunda forma encontra uma falha de concorrência que um teste sequencial não encontra.
Não use apenas um Set na memória para provar idempotência. Ele desaparece quando o processo reinicia e não coordena duas réplicas. A especificação do Standard Webhooks recomenda usar o ID da entrega como chave de idempotência. A decisão concreta pode ser uma restrição única no banco, uma operação atômica em uma fila ou outro armazenamento durável.
it("processes the same delivery once", async () => {
const request = makeWebhookRequest(
'{"type":"order.created","id":"evt_test_123"}',
"test-secret",
);
const [first, second] = await Promise.all([
handleWebhook(request, "test-secret"),
handleWebhook(request, "test-secret"),
]);
expect([first.status, second.status].sort()).toEqual([202, 202]);
expect(await countCreatedOrders("evt_test_123")).toBe(1);
});
O teste precisa observar o efeito que realmente importa. Contar chamadas de uma função mockada é insuficiente quando o bug está em uma gravação sem restrição, em uma fila ou em um pagamento disparado depois. Se o handler aceita a entrega e enfileira o trabalho, confirme também que a mensagem deduplicada não aparece duas vezes.
Não há experiência de cliente ou benchmark sendo alegado aqui. A regra prática deste artigo é um limite de verificação: se o teste não consegue dizer quantos efeitos foram produzidos para um ID repetido, ele ainda não prova idempotência.
Como testar retries sem esperar o relógio real?
Faça o adapter externo falhar de forma controlada e avance o relógio falso entre as tentativas. A suíte deve verificar o número de chamadas, os intervalos escolhidos, o status final e o efeito produzido. Não use sleep para esperar um retry de minutos terminar.
O Stripe documenta que um endpoint que não responde com sucesso pode receber novas tentativas e recomenda responder rapidamente antes de executar lógica complexa em "Receive Stripe events in your webhook endpoint". A regra exata varia por provedor, mas o teste deve deixar claro qual resposta provoca nova entrega no contrato escolhido.
| Cenário | Resposta do handler | O que verificar |
|---|---|---|
| processamento concluído | 200 ou 202 |
um efeito e nenhuma nova tentativa esperada |
| falha temporária antes do efeito | 5xx |
o provedor ou simulador tenta novamente |
| evento duplicado já persistido | 200 ou 202 |
nenhum efeito novo |
| payload inválido | 4xx |
a entrega não entra no fluxo de negócio |
| falha depois do efeito | contrato explícito | reconciliação ou deduplicação impede repetição |
Para separar o relógio da regra de retry, veja como testar lógica de retry em JavaScript sem esperar. O webhook acrescenta uma pergunta: o retry repete somente a entrega ou também repete uma chamada externa que já funcionou? Esse limite deve aparecer em um teste próprio.
E se os eventos chegarem fora de ordem?
Teste a ordem quando o estado de um recurso depende de mais de um evento. Envie, por exemplo, uma atualização antes do evento que cria o recurso e depois repita a sequência normal. O sistema precisa rejeitar, guardar, reconciliar ou aplicar os eventos com uma regra explícita. Silenciar a diferença não é uma política.
A documentação do GitHub lista entregas fora de ordem entre os problemas de webhook em "Troubleshooting webhooks". Ela também recomenda examinar a entrega e a resposta recebida pelo servidor. Em uma suíte própria, guarde o ID e o tipo de cada evento para que a falha mostre a sequência recebida.
Uma forma simples de testar isso é modelar o estado final esperado e comparar a consequência em vez da ordem das chamadas. Se o domínio exige monotonicidade, o evento precisa carregar uma versão ou um instante que o armazenamento consiga comparar. Se a ordem não pode ser garantida, o consumidor precisa buscar o estado atual ou agendar reconciliação.
Como separar testes locais e testes do provedor?
Use três camadas. A primeira testa o verificador, o parser e a regra de deduplicação com fixtures locais. A segunda atravessa o endpoint HTTP e verifica status, armazenamento e fila. A terceira usa o sandbox, CLI, replay ou entrega real de teste do provedor para conferir que o adapter local segue o protocolo dele.
A documentação do GitHub sobre testar webhooks descreve encaminhar entregas para um servidor local e inspecionar o que foi enviado e recebido. Esse caminho é útil para validar o formato real, mas não substitui os testes determinísticos que precisam rodar em todo pull request.
Também não transforme um mock de rede em prova de integração. O artigo sobre mocks de API no Playwright sem falsos positivos explica a diferença entre controlar o estado da interface e verificar o contrato real. No webhook, o mock pode produzir casos raros, enquanto uma chamada no sandbox confirma cabeçalhos, bytes e comportamento do provedor.
Guarde uma evidência pequena por cenário: entrada redigida, ID, status, motivo da decisão, estado final e contagem de efeitos. Não armazene segredos ou payloads com dados pessoais sem uma regra de retenção. O objetivo é tornar a falha reproduzível, não criar um segundo banco de produção dentro dos artefatos do CI.
Checklist antes de colocar o webhook no CI
Use esta ordem para revisar um handler novo ou uma mudança em uma integração existente:
- Fixe um payload representativo e preserve o corpo bruto usado na assinatura.
- Teste assinatura válida, ausente, alterada, expirada e assinada com segredo errado.
- Valide o schema antes de chamar a lógica de negócio.
- Confirme o comportamento para evento desconhecido e payload inválido.
- Entregue o mesmo ID duas vezes e confirme um único efeito.
- Entregue duas cópias em paralelo e exercite a deduplicação atômica.
- Force timeout e
5xxantes e depois do efeito para descobrir o contrato de retry. - Reproduza eventos fora de ordem e registre a decisão de reconciliação.
- Rode um teste no sandbox ou usando o mecanismo oficial de replay do provedor.
- Faça o CI publicar apenas evidência redigida e falhar quando uma promessa do contrato deixar de ser provada.
Se os testes de tempo ainda forem frágeis, controle datas e timers no JavaScript antes de aumentar retries do próprio runner. Repetir um teste pode revelar flakiness, mas não conserta uma deduplicação ausente ou uma assinatura calculada sobre o corpo errado.
Perguntas frequentes
Um teste que recebe 200 prova que o webhook funciona?
Não. O 200 prova apenas a resposta daquele cenário. Você ainda precisa provar assinatura, validação, duplicatas, falhas e o efeito de negócio. Um teste útil guarda o estado persistido e a quantidade de efeitos, além do código HTTP.
Devo testar com a API real do provedor?
Sim, em uma camada separada. O sandbox, CLI ou replay confere o protocolo real, mas é mais lento e sujeito a credenciais, rede e disponibilidade. Fixtures locais devem cobrir as combinações de erro no CI; o provedor deve confirmar o adapter e o formato da entrega.
Posso usar um mock para validar a assinatura?
Você pode mockar a fonte de entrega para testar a sua regra, mas o mock deve gerar os mesmos bytes e cabeçalhos do contrato. Se ele chama uma função que já retorna "assinatura válida", o teste não prova a integração. Mantenha uma amostra assinada e um teste negativo.
Qual é a melhor chave para deduplicar?
Use o identificador estável que o provedor define para uma entrega ou evento. Não derive a chave apenas do texto do payload se duas entregas legítimas puderem ter o mesmo conteúdo. Persista a decisão de forma atômica e confirme em teste que duas chamadas concorrentes produzem um efeito.
Conclusão
Um webhook confiável não é um endpoint que respondeu 200 uma vez. É uma fronteira testada com bytes preservados, assinatura verificada, payload validado, resposta clara, deduplicação durável, retries controlados e uma regra para ordem de eventos.
Comece pelas fixtures locais e pelos casos negativos. Depois atravesse o endpoint, simule concorrência e confirme o adapter no sandbox ou no replay oficial do provedor. Quando cada cenário deixa uma evidência pequena e verificável, o CI consegue proteger a integração sem fingir que um teste feliz representa a internet.
Fontes consultadas
- GitHub, "Testar webhooks" e "Solução de problemas de webhooks", consultado em 08/09/2026.
- Stripe, "Receive Stripe events in your webhook endpoint", consultado em 08/09/2026.
- Standard Webhooks, "Standard Webhooks specification", consultado em 08/09/2026.
- Node.js, "Crypto", consultado em 08/09/2026.
- Vitest, "Mocking Timers", consultado em 08/09/2026.