Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Prueba integraciones con Stripe usando claves API de modo test para llamadas reales a la API, Stripe CLI para probar webhooks localmente, el SDK de Stripe simulado para tests unitarios y Playwright para flujos de pago de extremo a extremo.
Claves API de modo test:
# .env.test
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_test_...Simula el SDK de Stripe para tests unitarios:
// __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;Tests de Vitest para Server Actions de Stripe:
// __tests__/actions/checkout.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { mockStripe } from "../../__mocks__/stripe";
// Simula el 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("creates a checkout session and redirects", async () => {
mockStripe.checkout.sessions.create.mockResolvedValue({
id: "cs_test_123",
url: "https://checkout.stripe.com/pay/cs_test_123",
});
// Importa después de configurar los mocks
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("handles Stripe errors gracefully", 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();
});
});Probar la creación 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("creates a payment intent with correct amount", 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 },
})
);
});
});Probar el manejador de webhooks:
// __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("handles 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("rejects invalid signatures", 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);
});
});Test E2E de Playwright para el flujo de checkout:
// e2e/checkout.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Checkout flow", () => {
test("completes a purchase with test card", async ({ page }) => {
await page.goto("/shop");
await page.click('button:has-text("Buy Now")');
// Espera la redirección a Stripe Checkout
await page.waitForURL(/checkout\.stripe\.com/);
// Rellena los datos de la tarjeta de prueba en 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");
// Rellena el nombre de facturación
const nameInput = page.locator('#billingName');
await nameInput.fill("Test User");
// Envía el pago
await page.click('button:has-text("Pay")');
// Espera la redirección de vuelta a la página de éxito
await page.waitForURL(/\/success/);
await expect(page.locator("h1")).toContainText("Payment Successful");
});
test("handles declined card", async ({ page }) => {
await page.goto("/checkout");
// Espera a que cargue el PaymentElement
const stripeFrame = page.frameLocator(
"iframe[name*='__privateStripeFrame']"
).first();
await stripeFrame
.locator('[placeholder="1234 1234 1234 1234"]')
.fill("4000000000000002"); // Tarjeta de rechazo 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 el mensaje de error
await expect(page.locator('[role="alert"]')).toContainText("declined");
});
});sk_test_ crean datos de prueba que no generan cargos reales.stripe listen --forward-to) crea un túnel que reenvía eventos de webhook desde el entorno de prueba de Stripe a tu servidor de desarrollo local.Stripe CLI para activar eventos específicos:
# Activa un evento específico
stripe trigger checkout.session.completed
# Activa con datos personalizados
stripe trigger payment_intent.succeeded \
--add payment_intent:metadata.userId=user_123
# Escucha y reenvía a un endpoint específico
stripe listen --forward-to localhost:3000/api/webhooks/stripe \
--events checkout.session.completed,invoice.paid
# Reenvía un evento específico desde tu Dashboard
stripe events resend evt_xxxProbar el ciclo de vida de una suscripción:
describe("Subscription lifecycle", () => {
it("handles subscription cancellation", 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> o usa vi.fn() con tipos de retorno explícitos para que coincidan con las respuestas esperadas de la API de Stripe.Stripe.Event. Usa formas de evento reales de la salida de Stripe CLI como referencia.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 de tarjeta | Comportamiento |
|---|---|
| 4242 4242 4242 4242 | Éxito |
| 4000 0000 0000 3220 | Requiere autenticación 3D Secure 2 |
| 4000 0025 0000 3155 | Requiere 3D Secure en setup |
| 4000 0000 0000 9995 | Rechazada: fondos insuficientes |
| 4000 0000 0000 0002 | Rechazada: rechazo genérico |
| 4000 0000 0000 0069 | Rechazada: tarjeta caducada |
| 4000 0000 0000 0127 | Rechazada: CVC incorrecto |
| 4000 0000 0000 0119 | Rechazada: error de procesamiento |
| 4000 0000 0000 0101 | Falla la verificación de CVC |
| 4000 0000 0000 3055 | Requiere 3D Secure, se completa con éxito |
| 4000 0000 0000 3063 | Requiere 3D Secure, falla la autenticación |
| 5555 5555 5555 4444 | Mastercard: éxito |
| 3782 822463 10005 | Amex: éxito |
Usa cualquier fecha de caducidad futura (p. ej., 12/34) y cualquier CVC de 3 dígitos (4 dígitos para Amex).
whsec_ y son distintos del secreto de webhook del Dashboard. Usa el secreto proporcionado por la CLI durante el desarrollo local.frameLocator para interactuar con ellos.vi.mock en Vitest se eleva (hoisting) al inicio del archivo. Los imports dinámicos (await import(...)) dentro de los tests garantizan que el módulo se cargue con los mocks en su sitio..env.test (en .gitignore) o variables de entorno de CI/CD.| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Vitest con Stripe simulado | Rápido, aislado, sin red | No prueba el comportamiento real de Stripe |
| Modo test de Stripe (API real) | Prueba la integración real | Más lento, requiere red, límites de tasa |
| Stripe CLI (webhooks) | Prueba el flujo real de webhooks localmente | Requiere configurar la CLI |
| Playwright E2E | Prueba el recorrido completo del usuario | Lento, inestable con iframes de Stripe |
| MSW (Mock Service Worker) | Intercepta HTTP, simulación realista | Más configuración que vi.mock |
sk_test_ y pk_test_; las de modo live empiezan por sk_live_ y pk_live_.__mocks__/stripe.ts que exporte un objeto mock con vi.fn() para cada método.vi.mock("@/lib/stripe", () => ({ stripe: mockStripe })) en tu archivo de test.await import(...) después de configurar los mocks para que surtan efecto.vi.mock se eleva al inicio del archivo, pero el módulo bajo prueba puede importar Stripe en el momento de la carga.// Firma válida
mockStripe.webhooks.constructEvent.mockReturnValue(event);
// Firma inválida
mockStripe.webhooks.constructEvent.mockImplementation(() => {
throw new Error("Invalid signature");
});constructEvent para que devuelva el evento o lance una excepción.stripe listen --forward-to localhost:3000/api/webhooks/stripe \
--events checkout.session.completed,invoice.paidstripe trigger checkout.session.completed.whsec_ para desarrollo local.4000 0000 0000 0002 activa un rechazo genérico.4000 0000 0000 3220 activa la autenticación 3D Secure 2.4242 4242 4242 4242 siempre tiene éxito.frameLocator para interactuar.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() sin genéricos explícitos: Vitest infiere el tipo a partir de los valores de retorno del mock.mockResolvedValue con objetos que coincidan con la interfaz Stripe.* relevante.Partial<Stripe> si necesitas un mock parcial del cliente completo.stripe listen y empieza por whsec_..env.test solo para desarrollo local.const stripeFrame = page.frameLocator(
"iframe[name*='__privateStripeFrame']"
).first();
await stripeFrame
.locator('[placeholder="1234 1234 1234 1234"]')
.fill("4242424242424242");page.frameLocator() con un selector que coincida con el iframe de Stripe.Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥