Testing
Testing Guide
Best practices for testing your QZPay integration.
Test Environment
Provider Test Modes
Always use test credentials during development:
# Stripe test keysSTRIPE_SECRET_KEY=sk_test_xxxSTRIPE_PUBLISHABLE_KEY=pk_test_xxx
# MercadoPago sandboxMP_ACCESS_TOKEN=TEST-xxxMP_PUBLIC_KEY=TEST-xxxIn-Memory Storage
For unit tests, use the mock storage:
import { createMockStorage } from '@qazuor/qzpay-core/testing';
const mockStorage = createMockStorage();
const billing = createQZPayBilling({ paymentAdapter: mockProvider, storage: mockStorage});Unit Testing
Testing Customer Operations
import { describe, it, expect, beforeEach } from 'vitest';import { createQZPayBilling } from '@qazuor/qzpay-core';import { createMockProvider, createMockStorage } from '@qazuor/qzpay-core/testing';
describe('CustomerService', () => { let billing: ReturnType<typeof createQZPayBilling>; let mockProvider: MockProvider; let mockStorage: MockStorage;
beforeEach(() => { mockProvider = createMockProvider(); mockStorage = createMockStorage(); billing = createQZPayBilling({ paymentAdapter: mockProvider, storage: mockStorage }); });
it('creates a customer', async () => { const customer = await billing.customers.create({ email: 'test@example.com', name: 'Test User' });
expect(customer.id).toBeDefined(); expect(customer.email).toBe('test@example.com'); expect(mockProvider.customers.create).toHaveBeenCalled(); expect(mockStorage.customers.create).toHaveBeenCalled(); });
it('retrieves a customer', async () => { const created = await billing.customers.create({ email: 'test@example.com' });
const retrieved = await billing.customers.get(created.id);
expect(retrieved.id).toBe(created.id); expect(retrieved.email).toBe('test@example.com'); });});Testing Subscription Logic
describe('SubscriptionService', () => { it('creates a subscription with trial', async () => { const customer = await billing.customers.create({ email: 'test@example.com' });
const subscription = await billing.subscriptions.create({ customerId: customer.id, planId: 'price_test', trialDays: 14 });
expect(subscription.status).toBe('trialing'); expect(subscription.trialEnd).toBeDefined(); });
it('cancels at period end', async () => { const subscription = await createActiveSubscription();
const cancelled = await billing.subscriptions.cancel(subscription.id, { atPeriodEnd: true });
expect(cancelled.cancelAtPeriodEnd).toBe(true); expect(cancelled.status).toBe('active'); // Still active until period end });});Testing Event Handlers
describe('Event Handlers', () => { it('emits subscription.created event', async () => { const handler = vi.fn(); billing.on('subscription.created', handler);
await billing.subscriptions.create({ customerId: 'cus_test', planId: 'price_test' });
expect(handler).toHaveBeenCalledWith( expect.objectContaining({ type: 'subscription.created', data: expect.objectContaining({ customerId: 'cus_test' }) }) ); });});Integration Testing
With Real Database
import { PostgreSqlContainer } from '@testcontainers/postgresql';import { drizzle } from 'drizzle-orm/postgres-js';import postgres from 'postgres';import { createQZPayDrizzleAdapter } from '@qazuor/qzpay-drizzle';
describe('Integration Tests', () => { let container: PostgreSqlContainer; let db: ReturnType<typeof drizzle>; let billing: ReturnType<typeof createQZPayBilling>;
beforeAll(async () => { container = await new PostgreSqlContainer().start();
const sql = postgres(container.getConnectionUri()); db = drizzle(sql);
// Run migrations await migrate(db, { migrationsFolder: './drizzle' });
billing = createQZPayBilling({ paymentAdapter: createMockProvider(), storage: createQZPayDrizzleAdapter(db) }); }, 60000);
afterAll(async () => { await container.stop(); });
it('persists customer to database', async () => { const customer = await billing.customers.create({ email: 'integration@test.com' });
const fromDb = await db.query.customers.findFirst({ where: eq(customers.id, customer.id) });
expect(fromDb).toBeDefined(); expect(fromDb?.email).toBe('integration@test.com'); });});With Real Provider (E2E)
import { createQZPayStripeAdapter } from '@qazuor/qzpay-stripe';
describe('Stripe E2E', () => { let billing: ReturnType<typeof createQZPayBilling>;
beforeAll(() => { // Use Stripe test mode billing = createQZPayBilling({ paymentAdapter: createQZPayStripeAdapter({ secretKey: process.env.STRIPE_TEST_SECRET_KEY! }), storage: createQZPayDrizzleAdapter(db) }); });
it('creates real Stripe customer', async () => { const customer = await billing.customers.create({ email: `e2e-${Date.now()}@test.com` });
expect(customer.externalIds.stripe).toMatch(/^cus_/);
// Cleanup await billing.customers.delete(customer.id); });});Webhook Testing
Mock Webhook Events
import { createMockWebhookEvent } from '@qazuor/qzpay-core/testing';
describe('Webhook Processing', () => { it('processes subscription.updated', async () => { const event = createMockWebhookEvent('customer.subscription.updated', { id: 'sub_123', status: 'past_due' });
// Test your webhook handler directly await yourSubscriptionHandler(event);
const subscription = await billing.subscriptions.get('sub_123'); expect(subscription.status).toBe('past_due'); });
it('handles payment.failed', async () => { const handler = vi.fn(); billing.on('payment.failed', handler);
const event = createMockWebhookEvent('invoice.payment_failed', { id: 'in_123', subscription: 'sub_123' });
// Test your webhook handler which triggers billing events await yourPaymentFailedHandler(event);
expect(handler).toHaveBeenCalled(); });});With Stripe CLI
# Forward webhooks to local serverstripe listen --forward-to localhost:3000/webhooks/stripe
# Trigger specific eventsstripe trigger customer.subscription.createdstripe trigger invoice.payment_failedstripe trigger charge.dispute.createdTest Cards
Stripe Test Cards
| Number | Description |
|---|---|
4242424242424242 | Successful payment |
4000000000000002 | Card declined |
4000002500003155 | Requires 3D Secure |
4000000000009995 | Insufficient funds |
4000000000000069 | Expired card |
MercadoPago Test Cards
| Number | Description |
|---|---|
5031 7557 3453 0604 | Approved |
5031 7557 3453 0620 | Pending |
5031 7557 3453 0612 | Rejected |
Coverage Goals
QZPay aims for high test coverage:
# Run coverage reportpnpm test:coverageTarget coverage:
- Statements: 90%+
- Branches: 85%+
- Functions: 90%+
- Lines: 90%+
Best Practices
- Use test containers for database tests
- Mock external APIs in unit tests
- Use real APIs only in E2E tests
- Test error paths as thoroughly as happy paths
- Verify event emissions for all operations
- Test webhook idempotency with duplicate events
- Clean up test data to avoid test pollution