Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Teste integrações com Stripe usando chaves de API em modo de teste para chamadas de API reais, Stripe CLI para testes locais de webhook, SDK do Stripe simulado para testes unitários e Playwright para fluxos de pagamento ponta a ponta.
Chaves de API em modo de teste:
# .env.test
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_test_...Simule o SDK do Stripe para testes unitários:
// __mocks__/stripe.ts
import { vi } from "vitest";
const mockStripe = {
checkout: {
sessions: {
create: vi.fn(),
retrieve: vi.fn(),
},
},
paymentIntents: {
create: vi.fn(),
retrieve: vi.fn(),
},
subscriptions: {
create: vi.fn(),
retrieve: vi.fn(),
update: vi.fn(),
cancel: vi.fn(),
},
customers: {
create: vi.fn(),
retrieve: vi.fn(),
},
billingPortal: {
sessions: {
create: vi.fn(),
},
},
webhooks: {
constructEvent: vi.fn(),
},
};
export default vi.fn(() => mockStripe);
export { mockStripe };// lib/__mocks__/stripe.ts
import { mockStripe } from "../../__mocks__/stripe";
export const stripe = mockStripe;Testes Vitest para Stripe Server Actions:
// __tests__/actions/checkout.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { mockStripe } from "../../__mocks__/stripe";
// Simula o módulo stripe
vi.mock("@/lib/stripe", () => ({
stripe: mockStripe,
}));
// Simula next/navigation
const mockRedirect = vi.fn();
vi.mock("next/navigation", () => ({
redirect: mockRedirect,
}));
describe("createCheckoutSession", () => {
beforeEach(() => {
vi.clearAllMocks();
process.env.NEXT_PUBLIC_APP_URL = "http://localhost:3000";
});
it("cria uma sessão de checkout e redireciona", async () => {
mockStripe.checkout.sessions.create.mockResolvedValue({
id: "cs_test_123",
url: "https://checkout.stripe.com/pay/cs_test_123",
});
// Importa após as simulações serem configuradas
const { createCheckoutSession } = await import(
"@/app/actions/checkout"
);
await createCheckoutSession("price_test_123");
expect(mockStripe.checkout.sessions.create).toHaveBeenCalledWith(
expect.objectContaining({
mode: "payment",
line_items: [{ price: "price_test_123", quantity: 1 }],
success_url: expect.stringContaining("/success"),
cancel_url: expect.stringContaining("/pricing"),
})
);
expect(mockRedirect).toHaveBeenCalledWith(
"https://checkout.stripe.com/pay/cs_test_123"
);
});
it("lida com erros do Stripe graciosamente", async () => {
mockStripe.checkout.sessions.create.mockRejectedValue(
new Error("No such price: 'price_invalid'")
);
const { createCheckoutSession } = await import(
"@/app/actions/checkout"
);
await expect(
createCheckoutSession("price_invalid")
).rejects.toThrow();
});
});Testando a criação de PaymentIntent:
// __tests__/actions/payment-intent.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { mockStripe } from "../../__mocks__/stripe";
vi.mock("@/lib/stripe", () => ({
stripe: mockStripe,
}));
describe("createPaymentIntent", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("cria um payment intent com o valor correto", async () => {
mockStripe.paymentIntents.create.mockResolvedValue({
id: "pi_test_123",
client_secret: "pi_test_123_secret_456",
amount: 2999,
currency: "usd",
status: "requires_payment_method",
});
const { createPaymentIntent } = await import(
"@/app/actions/payment"
);
const result = await createPaymentIntent(2999);
expect(result.clientSecret).toBe("pi_test_123_secret_456");
expect(mockStripe.paymentIntents.create).toHaveBeenCalledWith(
expect.objectContaining({
amount: 2999,
currency: "usd",
automatic_payment_methods: { enabled: true },
})
);
});
});Testando o manipulador de webhook:
// __tests__/api/webhook.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { mockStripe } from "../../__mocks__/stripe";
vi.mock("@/lib/stripe", () => ({
stripe: mockStripe,
}));
const mockDb = {
user: {
update: vi.fn(),
updateMany: vi.fn(),
},
purchase: {
create: vi.fn(),
},
};
vi.mock("@/lib/db", () => ({
db: mockDb,
}));
describe("Stripe webhook handler", () => {
beforeEach(() => {
vi.clearAllMocks();
process.env.STRIPE_WEBHOOK_SECRET = "whsec_test";
});
it("lida com checkout.session.completed", async () => {
const event = {
id: "evt_test_123",
type: "checkout.session.completed",
data: {
object: {
id: "cs_test_123",
mode: "subscription",
customer: "cus_test_123",
subscription: "sub_test_123",
metadata: { userId: "user_123" },
amount_total: 2900,
},
},
};
mockStripe.webhooks.constructEvent.mockReturnValue(event);
const { POST } = await import(
"@/app/api/webhooks/stripe/route"
);
const request = new Request("http://localhost:3000/api/webhooks/stripe", {
method: "POST",
body: JSON.stringify(event),
headers: { "stripe-signature": "test_sig" },
});
const response = await POST(request);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.received).toBe(true);
});
it("rejeita assinaturas inválidas", async () => {
mockStripe.webhooks.constructEvent.mockImplementation(() => {
throw new Error("Invalid signature");
});
const { POST } = await import(
"@/app/api/webhooks/stripe/route"
);
const request = new Request("http://localhost:3000/api/webhooks/stripe", {
method: "POST",
body: "{}",
headers: { "stripe-signature": "invalid" },
});
const response = await POST(request);
expect(response.status).toBe(400);
});
});Teste E2E do Playwright para o fluxo de checkout:
// e2e/checkout.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Fluxo de Checkout", () => {
test("completa uma compra com cartão de teste", async ({ page }) => {
await page.goto("/shop");
await page.click('button:has-text("Buy Now")');
// Aguarda o redirecionamento do Stripe Checkout
await page.waitForURL(/checkout\.stripe\.com/);
// Preenche os detalhes do cartão de teste no Stripe Checkout
const emailInput = page.locator("#email");
await emailInput.fill("test@example.com");
const cardFrame = page.frameLocator("iframe[name*='card']").first();
await cardFrame
.locator('[placeholder="1234 1234 1234 1234"]')
.fill("4242424242424242");
await cardFrame
.locator('[placeholder="MM / YY"]')
.fill("12/34");
await cardFrame
.locator('[placeholder="CVC"]')
.fill("123");
// Preenche o nome de faturamento
const nameInput = page.locator('#billingName');
await nameInput.fill("Test User");
// Submete o pagamento
await page.click('button:has-text("Pay")');
// Aguarda o redirecionamento de volta para a página de sucesso
await page.waitForURL(/\/success/);
await expect(page.locator("h1")).toContainText("Payment Successful");
});
test("lida com cartão recusado", async ({ page }) => {
await page.goto("/checkout");
// Aguarda o carregamento do PaymentElement
const stripeFrame = page.frameLocator(
"iframe[name*='__privateStripeFrame']"
).first();
await stripeFrame
.locator('[placeholder="1234 1234 1234 1234"]')
.fill("4000000000000002"); // Cartão de recusa genérico
await stripeFrame
.locator('[placeholder="MM / YY"]')
.fill("12/34");
await stripeFrame
.locator('[placeholder="CVC"]')
.fill("123");
await page.click('button:has-text("Pay")');
// Espera a mensagem de erro
await expect(page.locator('[role="alert"]')).toContainText("declined");
});
});sk_test_ criam dados de teste que não resultam em cobranças reais.stripe listen --forward-to) cria um túnel que encaminha eventos de webhook do ambiente de teste do Stripe para o seu servidor de desenvolvimento local.Stripe CLI para disparar eventos específicos:
# Dispara um evento específico
stripe trigger checkout.session.completed
# Dispara com dados personalizados
stripe trigger payment_intent.succeeded \
--add payment_intent:metadata.userId=user_123
# Escuta e encaminha para um endpoint específico
stripe listen --forward-to localhost:3000/api/webhooks/stripe \
--events checkout.session.completed,invoice.paid
# Reexecuta um evento específico do seu Dashboard
stripe events resend evt_xxxTestando o ciclo de vida da assinatura:
describe("Ciclo de vida da assinatura", () => {
it("lida com o cancelamento da assinatura", async () => {
const mockUser = {
id: "user_123",
stripeSubscriptionId: "sub_test_123",
};
mockStripe.subscriptions.update.mockResolvedValue({
id: "sub_test_123",
cancel_at_period_end: true,
status: "active",
});
vi.mock("@/lib/auth", () => ({
auth: vi.fn().mockResolvedValue({
user: { id: "user_123" },
}),
}));
vi.mock("@/lib/db", () => ({
db: {
user: {
findUnique: vi.fn().mockResolvedValue(mockUser),
},
},
}));
const { cancelSubscriptionAction } = await import(
"@/app/actions/subscription"
);
const result = await cancelSubscriptionAction();
expect(result).toEqual({ success: true });
expect(mockStripe.subscriptions.update).toHaveBeenCalledWith(
"sub_test_123",
{ cancel_at_period_end: true }
);
});
});Partial<Stripe> ou use vi.fn() com tipos de retorno explícitos para corresponder às respostas esperadas da API do Stripe.Stripe.Event. Use formatos de evento reais da saída do Stripe CLI como referência.import type Stripe from "stripe";
function createMockEvent(
type: string,
data: Record<string, unknown>
): Stripe.Event {
return {
id: `evt_test_${Date.now()}`,
type,
data: { object: data },
object: "event",
api_version: "2024-12-18.acacia",
created: Math.floor(Date.now() / 1000),
livemode: false,
pending_webhooks: 0,
request: null,
} as unknown as Stripe.Event;
}| Número do Cartão | Comportamento |
|---|---|
| 4242 4242 4242 4242 | Sucesso |
| 4000 0000 0000 3220 | Autenticação 3D Secure 2 necessária |
| 4000 0025 0000 3155 | 3D Secure necessário na configuração |
| 4000 0000 0000 9995 | Recusado: fundos insuficientes |
| 4000 0000 0000 0002 | Recusado: recusa genérica |
| 4000 0000 0000 0069 | Recusado: cartão expirado |
| 4000 0000 0000 0127 | Recusado: CVC incorreto |
| 4000 0000 0000 0119 | Recusado: erro de processamento |
| 4000 0000 0000 0101 | Falha na verificação do CVC |
| 4000 0000 0000 3055 | 3D Secure necessário, completa com sucesso |
| 4000 0000 0000 3063 | 3D Secure necessário, falha na autenticação |
| 5555 5555 5555 4444 | Mastercard: sucesso |
| 3782 822463 10005 | Amex: sucesso |
Use qualquer data de expiração futura (por exemplo, 12/34) e qualquer CVC de 3 dígitos (4 dígitos para Amex).
whsec_ e são diferentes do segredo do webhook do seu Dashboard. Use o segredo fornecido pelo CLI durante o desenvolvimento local.frameLocator para interagir com eles.vi.mock no Vitest é "hoisted" para o topo do arquivo. Importações dinâmicas (await import(...)) dentro dos testes garantem que o módulo seja carregado com as simulações em vigor..env.test (ignorado pelo git) ou variáveis de ambiente CI/CD.| Abordagem | Prós | Contras |
|---|---|---|
| Vitest com Stripe simulado | Rápido, isolado, sem necessidade de rede | Não testa o comportamento real do Stripe |
| Modo de teste do Stripe (API real) | Testa a integração real | Mais lento, precisa de rede, limites de taxa |
| Stripe CLI (webhooks) | Testa o fluxo real de webhook localmente | Requer configuração do CLI |
| Playwright E2E | Testa a jornada completa do usuário | Lento, instável com iframes do Stripe |
| MSW (Mock Service Worker) | Intercepta HTTP, simulação realista | Mais configuração do que vi.mock |
sk_test_ e pk_test_; as chaves ativas começam com sk_live_ e pk_live_.__mocks__/stripe.ts que exporta um objeto simulado com vi.fn() para cada método.vi.mock("@/lib/stripe", () => ({ stripe: mockStripe })) em seu arquivo de teste.await import(...) após as simulações serem configuradas para que as simulações tenham efeito.vi.mock é "hoisted" para o topo do arquivo, mas o módulo a ser testado pode importar o Stripe no momento do carregamento.// Assinatura válida
mockStripe.webhooks.constructEvent.mockReturnValue(event);
// Assinatura inválida
mockStripe.webhooks.constructEvent.mockImplementation(() => {
throw new Error("Invalid signature");
});constructEvent para retornar o evento ou lançar um erro.stripe listen --forward-to localhost:3000/api/webhooks/stripe \
--events checkout.session.completed,invoice.paidstripe trigger checkout.session.completed.whsec_ para desenvolvimento local.4000 0000 0000 0002 dispara uma recusa genérica.4000 0000 0000 3220 dispara autenticação 3D Secure 2.4242 4242 4242 4242 sempre tem sucesso.frameLocator para interagir.rate_limit_error.import type Stripe from "stripe";
function createMockEvent(
type: string,
data: Record<string, unknown>
): Stripe.Event {
return {
id: `evt_test_${Date.now()}`,
type,
data: { object: data },
object: "event",
api_version: "2024-12-18.acacia",
created: Math.floor(Date.now() / 1000),
livemode: false,
pending_webhooks: 0,
request: null,
} as unknown as Stripe.Event;
}vi.fn() sem genéricos explícitos - Vitest infere o tipo dos valores de retorno da simulação.mockResolvedValue com objetos que correspondam à interface Stripe.* relevante.Partial<Stripe> se precisar de uma simulação parcial do cliente completo.stripe listen e começa com whsec_..env.test apenas para desenvolvimento local.const stripeFrame = page.frameLocator(
"iframe[name*='__privateStripeFrame']"
).first();
await stripeFrame
.locator('[placeholder="1234 1234 1234 1234"]')
.fill("4242424242424242");page.frameLocator() com um seletor que corresponda ao iframe do Stripe.Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥