
Больше нет «корабля и молитвы»: тестирование биллинговых систем SaaS с тестовыми часами Playwright & Stripe
20 августа 2025 г.Представьте себе это: вы только что внедрили новую функцию подписки. Ваши пользователи получают 7-дневную пробную версию, а затем 250 долларов в месяц за ваш профессиональный план. QA задает страшный вопрос: «Как мы проверяем весь жизненный цикл выставления счетов?»
Ваши варианты кажутся ограниченными:
- Подождите 7+ днейДля тестирования в реальном времени (и еще 30+ для следующего цикла)
- Вручную манипулировать временными метками базы данных(хрупкий и нереалистичный)
- Все высмеивайте(Но пропустите проблемы интеграции с реальной полосой)
- Корабль и молитесь(Мы все были там 😅)
Каждый подход имеет серьезные недостатки. Тестирование в реальном времени слишком медленное для CI/CD. Манипуляции с базой данных разрываются при участии веб -крючков. Измешивание пропускает реальное поведение полосы. И доставка непроверенного биллингового кода? Вот как вы становитесь злыми клиентами и возвратами платежей.
Что если я скажу вам, что есть лучший способ?
Поврежание игры: тестовые часы полосы + Playwright E2E тестирование
Мы решили эту проблему, объединив тестовые часы полосы с сквозным тестированием драматурга, чтобы создать комплексную систему проверки выставления счетов, которая работает менее чем за 5 минут.
Наше решение позволяет нам:
- Проверьте полные жизненные циклы выставления счетов (пробная версия → оплата → продление) в считанные минуты
- Проверьте точные суммы счетов и переходы на статус подписки
- Проверьте сценарии обработки и синхронизации Webhook
- Поймать ошибки с выставлением счетов, прежде чем они достигнут производства
- Запустите детерминированные тесты, которые работают в любой среде
Вот как мы его построили, и как вы тоже можете.
Архитектура: изолированная окружающей средой манипуляции с временем
Наша реализация Test Clock состоит из трех основных компонентов:
1. Бэкэнд слой API
Выделенный API управления тестовыми часами, который обрабатывает:
- Безопасность окружающей среды:Тестовые часы работают только в условиях тестирования/разработки
- Управление ресурсами:Создание, продвижение и очистка тестовых часов
- Координация веб -крюка:Управление временем обработки stripe webhook
2.
Утилиты для типа
- Тестовая среда создания:Полная настройка теста с вызовом за один вызов
- Время продвижения:Продвижение времени и дождитесь обработки веб -крючков
- Проверка счетов:Комплексная проверка счета и подписки
3. Playwright E2E интеграция
Сквозные тесты, которые проверяют полное путешествие пользователя:
- Взаимодействие пользовательского интерфейса:Реальные пользовательские записи и подписные потоки
- Тестовые манипуляции с часами:Закулисное время продвижения
- Проверка счетов:Автоматизированная проверка биллинговых мероприятий
Технические глубокие погружения: детали реализации
Бэкэнд: API управления тестовыми часами
Наш бэкэнд обеспечивает комплексный API REST для испытательных часов. Вот основная структура:
export const createTestClock = async (frozenTime, name = null) => {
// Environment safety check
ensureTestEnvironment();
const stripe = getStripeClient();
const frozenTimeUnix = Math.floor(frozenTime.getTime() / 1000);
const testClock = await stripe.testHelpers.testClocks.create({
frozen_time: frozenTimeUnix,
name: name || `test-${Date.now()}`
});
logger.info('Created test clock', {
testClockId: testClock.id,
frozenTime: frozenTime.toISOString()
});
return testClock;
};
export const advanceTestClock = async (testClockId, targetTime) => {
ensureTestEnvironment();
const stripe = getStripeClient();
const targetTimeUnix = Math.floor(targetTime.getTime() / 1000);
const result = await stripe.testHelpers.testClocks.advance(testClockId, {
frozen_time: targetTimeUnix
});
logger.info('Advanced test clock', {
testClockId,
targetTime: targetTime.toISOString()
});
return result;
};
Сервисный слой организует сложные операции:
// testClocks.service.js - Business process orchestration
export const createBillingTestEnvironment = async (
accountId,
tier,
billingPeriod,
frozenTime,
options = {}
) => {
return withTransaction(async (session) => {
// Create test clock
const testClock = await createTestClock(frozenTime,
`billing-test-${accountId}-${Date.now()}`);
// Create Stripe customer with test clock
const customer = await createOrUpdateStripeCustomer(accountId, {
test_clock: testClock.id
});
// Create subscription with trial
const priceId = getTierPriceId(tier, billingPeriod);
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{ price: priceId }],
trial_period_days: options.trialDays || 7,
payment_behavior: 'default_incomplete',
payment_settings: { save_default_payment_method: 'on_subscription' }
});
return {
testClock: {
id: testClock.id,
frozenTime: frozenTime,
frozenTimeUnix: testClock.frozen_time
},
customer: { id: customer.id, accountId },
subscription: {
id: subscription.id,
status: subscription.status,
trialStart: subscription.trial_start,
trialEnd: subscription.trial_end
}
};
});
};
Frontend: TypeScript Test Helpers
Наши помощники на фронте обеспечивают чистый API для тестовых часов с часами:
// stripe-test-helpers.ts
export interface TestClockEnvironment {
testClock: {
id: string;
frozenTime: string;
frozenTimeUnix: number;
};
customer: { id: string; accountId: string };
subscription: {
id: string;
status: string;
trialStart?: string;
trialEnd?: string;
};
}
export async function createTestClockEnvironment(
page: Page,
accountId: string,
tier: string,
billingPeriod: 'monthly' | 'yearly',
frozenTime: string,
options: { includeTrial?: boolean; trialDays?: number } = {}
): Promise<TestClockEnvironment> {
const authToken = await getAuthToken(page);
const response = await page.request.post('/api/v1/billing/test/environments', {
data: {
accountId,
tier,
billingPeriod,
frozenTime,
includeTrial: options.includeTrial ?? true,
trialDays: options.trialDays ?? 7
},
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
const result = await response.json();
return result.data;
}
export async function advanceTestClockAndWaitForWebhooks(
page: Page,
testClockId: string,
targetTime: string,
webhookTimeout: number = 30000
): Promise<any> {
const authToken = await getAuthToken(page);
const response = await page.request.put(
`/api/v1/billing/test/environments/${testClockId}/advance`,
{
data: { targetTime, webhookTimeout },
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
}
);
const result = await response.json();
console.log('✅ Test clock advanced:', {
testClockId,
targetTime,
webhooksProcessed: result.data.processing.webhooksProcessed
});
return result.data;
}
Расширенная стратегия обработки веб -крюков
Одной из самых больших проблем с тестовыми часами является время WebHook. Stripe обрабатывает WebHooks асинхронно, поэтому продолжение времени не гарантирует немедленную доставку веб -крючков. Наше решение использует многослойный подход:
export const waitForWebhookProcessing = async (
testClockId,
timeout = 30000
) => {
const startTime = Date.now();
const pollInterval = 2000;
while (Date.now() - startTime < timeout) {
// Check if webhooks are still processing
const testClock = await getTestClockStatus(testClockId);
if (testClock.status === 'ready') {
// Allow additional buffer for webhook processing
await new Promise(resolve => setTimeout(resolve, 3000));
return { completed: true, processingTime: Date.now() - startTime };
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
throw new Error(`Webhook processing timeout after ${timeout}ms`);
};
Комплексная проверка счетов
Наша система проверки проверяет несколько аспектов биллинговых событий:
export interface BillingVerification {
subscription: {
status: string;
statusMatches: boolean;
currentPeriodStart: string;
currentPeriodEnd: string;
};
invoices: {
total: number;
hasInvoices: boolean;
validBillingEvents: number;
details: Array<{
id: string;
status: string;
amountDue: number;
amountPaid: number;
paid: boolean;
isValidBillingEvent: boolean;
effectiveAmount: number;
}>;
};
verification: {
subscriptionStatusOK: boolean;
invoicesCreatedOK: boolean;
invoicesPaidOK: boolean;
overallSuccess: boolean;
};
}
export async function verifyBillingLifecycle(
page: Page,
customerId: string,
subscriptionId: string,
expectations: {
invoiceCreated?: boolean;
invoicePaid?: boolean;
subscriptionStatus?: string;
} = {}
): Promise<BillingVerification> {
const authToken = await getAuthToken(page);
const response = await page.request.post('/api/v1/billing/test/verify', {
data: { customerId, subscriptionId, expectations },
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
const result = await response.json();
console.log('📊 Billing verification results:', {
overallSuccess: result.data.verification.overallSuccess,
subscriptionStatus: result.data.subscription.status,
invoiceCount: result.data.invoices.total
});
return result.data;
}
Пример реального мира: полный тест на жизненный цикл счетов.
Вот полный тест драматурга, который проверяет наш план Pro Plan (250 долл. США в месяц) от судебного разбирательства до второго платежа:
test('should correctly charge invoice after trial period using test clocks', async () => {
test.setTimeout(300000); // 5 minutes max
const subscriptionPage = new SubscriptionManagementPage(page);
let testEnvironment: TestClockEnvironment;
// Step 1: Create test clock environment with trial
await test.step('Create test clock environment', async () => {
const frozenTime = new Date();
frozenTime.setHours(0, 0, 0, 0); // Start of today
testEnvironment = await createTestClockEnvironment(
page,
accountId,
'pro',
'monthly',
frozenTime.toISOString(),
{ includeTrial: true, trialDays: 7 }
);
console.log('✅ Test environment created:', {
testClockId: testEnvironment.testClock.id,
customerId: testEnvironment.customer.id,
subscriptionId: testEnvironment.subscription.id
});
});
// Step 2: User upgrades through UI (with test clock injection)
await test.step('User upgrades to Pro plan', async () => {
// Intercept checkout API to inject test clock ID
await page.route('**/api/v1/billing/checkout', async route => {
const request = route.request();
if (request.method() === 'POST') {
const originalData = request.postDataJSON();
const modifiedData = {
...originalData,
testClockId: testEnvironment.testClock.id
};
await route.continue({
postData: JSON.stringify(modifiedData),
headers: { ...request.headers(), 'Content-Type': 'application/json' }
});
} else {
await route.continue();
}
});
// Complete upgrade through UI
await subscriptionPage.upgradeToProPlan('monthly');
await subscriptionPage.waitForStripeRedirect();
// Complete Stripe checkout
await completeStripeCheckout(page, STRIPE_TEST_CARDS.VISA_SUCCESS);
console.log('✅ Pro plan upgrade completed through UI');
});
// Step 3: Verify trial status ($0 invoice)
await test.step('Verify trial status', async () => {
const trialVerification = await verifyBillingLifecycle(
page,
testEnvironment.customer.id,
testEnvironment.subscription.id,
{ subscriptionStatus: 'trialing' }
);
// Should be in trial with $0 invoices
expect(trialVerification.subscription.status).toBe('trialing');
const chargedInvoices = trialVerification.invoices.details.filter(
invoice => invoice.amountDue > 0 || invoice.amountPaid > 0
);
expect(chargedInvoices.length).toBe(0);
console.log('✅ Trial verified: No charges during trial period');
});
// Step 4: Advance time past trial (8 days)
await test.step('Advance past trial period', async () => {
const trialEndTime = new Date();
trialEndTime.setHours(0, 0, 0, 0);
trialEndTime.setTime(trialEndTime.getTime() + (8 * 24 * 60 * 60 * 1000));
await advanceTestClockAndWaitForWebhooks(
page,
testEnvironment.testClock.id,
trialEndTime.toISOString(),
90000 // 90 second webhook timeout
);
console.log('✅ Time advanced 8 days past trial start');
});
// Step 5: Verify first $250 payment
await test.step('Verify first Pro plan payment', async () => {
const postTrialVerification = await verifyBillingLifecycle(
page,
testEnvironment.customer.id,
testEnvironment.subscription.id,
{ subscriptionStatus: 'active' }
);
// Should now be active (not trialing)
expect(postTrialVerification.subscription.status).toBe('active');
// Should have $250 Pro plan charge
const proInvoices = postTrialVerification.invoices.details.filter(
inv => inv.isValidBillingEvent && inv.effectiveAmount === 25000 // $250 in cents
);
expect(proInvoices.length).toBe(1);
console.log('✅ First Pro plan payment verified: $250 charged');
});
// Step 6: Advance to second billing cycle (32 more days)
await test.step('Advance to second billing cycle', async () => {
const secondBillingTime = new Date();
secondBillingTime.setHours(0, 0, 0, 0);
secondBillingTime.setTime(secondBillingTime.getTime() + (40 * 24 * 60 * 60 * 1000));
await advanceTestClockAndWaitForWebhooks(
page,
testEnvironment.testClock.id,
secondBillingTime.toISOString(),
90000
);
console.log('✅ Advanced to second billing cycle (40 days total)');
});
// Step 7: Verify second $250 payment
await test.step('Verify second Pro plan payment', async () => {
const secondBillingVerification = await verifyBillingLifecycle(
page,
testEnvironment.customer.id,
testEnvironment.subscription.id,
{ subscriptionStatus: 'active' }
);
// Should have 2 Pro plan billing events now
const proInvoices = secondBillingVerification.invoices.details.filter(
inv => inv.isValidBillingEvent && inv.effectiveAmount === 25000
);
expect(proInvoices.length).toBe(2);
console.log('✅ Second Pro plan payment verified: Total $500 charged');
console.log('🎉 Complete billing lifecycle test passed!');
});
// Step 8: Cleanup test environment
await test.step('Cleanup test environment', async () => {
await cleanupTestClockEnvironment(
page,
testEnvironment.testClock.id,
true // Cancel subscriptions
);
console.log('✅ Test environment cleaned up');
});
});
Этот единственный тест проверяет:
- Пробный период с платой $ 0
- Первая оплата после заканчивая пробной версии (250 долларов США)
- Второй ежемесячный платеж (250 долларов США)
- Правильные переходы статуса подписки
- Обработка и сроки веб -крючков
- Генерация счетов и обработка платежей
Общее время теста: менее 5 минут против 40+ дней в режиме реального времени
Усовершенствованные функции: помимо базовых манипуляций с временем
Механизмы безопасности окружающей среды
Одним из важнейших аспектов нашей реализации является обеспечение тестовых часов никогда не выполняться в производстве:
export const ensureTestEnvironment = () => {
const nodeEnv = process.env.NODE_ENV;
if (nodeEnv !== 'test' && nodeEnv !== 'development') {
logger.error('Test clocks attempted in production', { nodeEnv });
throw new BadRequestError(
'Test clocks are only available in test and development environments',
'INVALID_ENVIRONMENT'
);
}
};
// Applied at multiple layers:
// 1. Function level (every test clock operation)
// 2. Route middleware (API endpoint protection)
// 3. Environment variable validation (startup checks)
Комплексная очистка ресурсов
Тестовые часы могут накапливаться с течением времени, поэтому мы создали надежные механизмы очистки:
export const cleanupTestClockEnvironment = async (
testClockId,
cancelSubscriptions = true
) => {
const results = { overallSuccess: true, details: {} };
try {
// Cancel associated subscriptions
if (cancelSubscriptions) {
const subscriptions = await getTestClockSubscriptions(testClockId);
for (const subscription of subscriptions) {
await stripe.subscriptions.cancel(subscription.id);
}
results.details.subscriptionsCanceled = subscriptions.length;
}
// Delete the test clock (automatically cleans up associated resources)
await stripe.testHelpers.testClocks.del(testClockId);
results.details.testClockDeleted = true;
logger.info('Test clock environment cleaned up', { testClockId });
} catch (error) {
results.overallSuccess = false;
results.details.error = error.message;
logger.warn('Test clock cleanup had issues', { testClockId, error });
}
return results;
};
// Automatic cleanup in test hooks
test.afterAll(async () => {
if (testEnvironment?.testClock?.id) {
await cleanupTestClockEnvironment(testEnvironment.testClock.id);
}
});
Обработка ошибок и логика повторения
Тесты выставления счетов часто включают внешние вызовы API, которые могут быть сложенными. Наша реализация включает в себя комплексную обработку ошибок:
export async function advanceTestClockWithRetry(
page: Page,
testClockId: string,
targetTime: string,
maxRetries: number = 3
): Promise<any> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await advanceTestClockAndWaitForWebhooks(page, testClockId, targetTime);
} catch (error) {
console.warn(`Attempt ${attempt}/${maxRetries} failed:`, error.message);
if (attempt === maxRetries) {
throw new Error(`Test clock advancement failed after ${maxRetries} attempts: ${error.message}`);
}
// Exponential backoff
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempt - 1)));
}
}
}
Метрики производительности: цифры не лгут
Вот что обеспечивает наша реализация Test Clock Stripe:
Экономия времени Традиционное тестирование:
- Традиционное тестирование: 40+ дней в режиме реального времени для полного цикла выставления счетов
- Наше решение: 4-5 минут автоматизированное тестирование
- Улучшение: На 99,99% быстрее проверки счетов.
Улучшения покрытия
- До: Только ручное тестирование только счастливого пути
- После: Автоматизированное тестирование полного жизненного цикла выставления счетов, включая:
- Пробные периоды и переходы
- Многочисленные циклы счетов
- Сбои платежей и восстановление
- Обновления подписки/понижения
- Случаи по обработке Webhook Edge
Достоверность надежности
- Детерминированное тестирование: Те же результаты каждый раз
- Изоляция окружающей среды: Нет вмешательства между тестами
- Комплексная проверка: Суммы счетов, статус подписки, обработка веб -крючков
- Раннее обнаружение ошибок: Поймать проблемы с выставлением счетов перед производством
Опыт разработчика
- Простой API: Создание среды испытаний в одной линии
- Поддержка TypeScript: Безопасность и Intellisense полного типа
- Автоматическая очистка: Нет испытательных загрязнений или утечек ресурсов
- Богатый журнал: Подробное понимание выполнения теста
Извлечены уроки: что мы делали по -другому
Ключевые архитектурные решения
1. Сначала изоляция окружающей среды
Мы рано узнали, что тестовые часы являются мощными и потенциально опасными. Построение ограничений окружающей среды в каждый слой имело решающее значение для душевного спокойствия.
2. Стратегия обработки веб -хоуков
Доставка веб -крючки Stripe асинхронна, а время варьируется. Мы попробовали несколько подходов:
- ❌ Фиксированные задержки (ненадежные)
- ❌ Одиночная попытка опроса (пропущенные медленные веб -крючки)
- ✅ Конфигурируемый опрос с экспоненциальным отбором (надежный)
3. Дизайн очистки ресурсов
Тестовые часы быстро накапливаются во время разработки. Мы встроили очистку в:
- Индивидуальный тестовый разрыв
- Конечные конечные точки очистки для технического обслуживания
- Обнаружение осиротевших ресурсов
- Изящная обработка сбоев (очистка не должна преодолеть тесты)
Проблемы преодолевают
Задача 1: координация времени Webhook Timing
Стрипе обрабатывают веб -крючки асинхронно после тестового развития часов. Первоначальные попытки продвигать время и немедленно проверять результаты часто не удались.
Решение: Реализованная проверка на основе опросов с настраиваемыми тайм-аутами и несколькими стратегиями повторения.
Вызов 2: Загрязнение среды тестирования
Тестовые часы и подписки сохранялись между тестовыми прогонами, вызывая помехи и ложные срабатывания.
Решение: Комплексные механизмы очистки с автоматическим разрывом в тестовых крючках и ручной конечной точкам очистки.
Вызов 3: Обнаружение сложного выставления счетов
Stripe создает различные типы счетов (пробные счеты в $ 0, проекты счетов, оплачиваемые счета), которые создали логический комплекс проверки.
Решение: Усовершенствованная проверка выставления счетов, которая классифицирует счета по типу и подтверждает «эффективные суммы» для истинных событий выставления счетов.
Заключение: тестирование со скоростью развития
Строительство надежных систем платежного платежа по подписке не должно быть месяцами испытаний ручного тестирования и горячих всплесков производства. С помощью тестовых часов и вдумчивой архитектуры тестирования E2E вы можете проверить сложные жизненные циклы выставления счетов за считанные минуты вместо месяцев.
Наша реализация демонстрирует, что с правильными абстракциями и механизмами безопасности тестирование манипулирования временем может быть как мощным, так и безопасным. Ключевое понимание:
- Изоляция окружающей среды не подлежит обсуждению- Сначала строить механизмы безопасности
- Обработка веб -крюков требует терпения- Дизайн для асинхронных операций
- Очистка ресурсов имеет решающее значение- Планируйте управление тестовой средой
- Богатая проверка бьет простые утверждения- Создание комплексной проверки
- Опыт разработчика имеет значение- Простые APIS включить сложное тестирование
Результат?На 99,99% быстрее проверки счетов.Это ловит ошибки до того, как они достигнут производства и дают разработчикам доверие к отправке платежных функций с скоростью современной разработки программного обеспечения.
Об этой реализации
Эта статья основана на реальной реализации производства, используемой для тестирования системы подписки на платформу SaaS. Примеры кода упрощены для ясности, но представляют фактическую архитектуру и шаблоны, используемые в производстве.
Оригинал