Skip to content
revenue-sdk
Esc
navigateopen⌘Jpreview
On this page

Testing provider

Exercise your billing integration with the seedable in-memory provider from revenue-sdk/testing — seeds, state assertions, and capability overrides.

revenue-sdk/testing ships createInMemoryProvider, an in-memory RevenueProvider you can hand to createClient to test your integration without hitting a real billing provider — no fetch stubs, no sandbox accounts, no test cards.

Basic usage

import { createClient } from 'revenue-sdk';
import { createInMemoryProvider } from 'revenue-sdk/testing';

const provider = createInMemoryProvider({
  products: [{ id: 'pro', name: 'Pro', prices: [{ amount: 2900, interval: 'month' }] }],
  customers: [{ id: 'cus-1', email: '[email protected]' }],
  subscriptions: [{ id: 'sub-1', customerId: 'cus-1', productId: 'pro', status: 'active' }],
});

const client = createClient({ provider });

const subscription = await client.subscriptions.get({ id: 'sub-1' });
subscription.status; // 'active'

The seed is optional — createInMemoryProvider() starts empty.

The seed shape

products, customers, and subscriptions are arrays; every field is optional and gets a sensible default. Missing IDs are generated (product-1, price-2, customer-3, …).

PropType
products?InMemoryProductSeed[]

{ id?, name?, description?, prices? }. name defaults to the id.

TypeInMemoryProductSeed[]
customers?InMemoryCustomerSeed[]

{ id?, email?, name?, metadata? }. email defaults to <id>@example.com.

TypeInMemoryCustomerSeed[]
subscriptions?InMemorySubscriptionSeed[]

{ id?, status?, cancelAtPeriodEnd?, customerId?, productId?, quantity?, currency?, amount?, interval?, currentPeriodEnd?, trialEndsAt?, endsAt?, metadata? }.

TypeInMemorySubscriptionSeed[]

Price seeds default to a fixed, recurring, monthly price of 1000 in usd, and checkoutRef defaults to the parent product’s ID:

PropType
id?string

Generated when omitted.

Typestring
checkoutRef?string

Defaults to the parent product's id.

Typestring
type?'one_time' | 'recurring'

Price type.

Type'one_time' | 'recurring'
Defaultrecurring
model?PriceModel

Pricing model.

TypePriceModel
Defaultfixed
amount?number | null

Minor units. Pass null explicitly for non-fixed models.

Typenumber | null
Default1000
currency?string

Lowercase ISO 4217 code.

Typestring
Defaultusd
interval?BillingInterval

Omitted when type is 'one_time'.

TypeBillingInterval
Defaultmonth
intervalCount?number

Intervals per billing cycle.

Typenumber
trialDays?number

Trial length in days.

Typenumber

A fuller seed:

const provider = createInMemoryProvider({
  products: [
    {
      id: 'pro-monthly',
      name: 'Pro (monthly)',
      prices: [{ id: 'price-monthly', amount: 2900, currency: 'usd', interval: 'month' }],
    },
    {
      id: 'pro-yearly',
      name: 'Pro (yearly)',
      prices: [{ id: 'price-yearly', amount: 29000, currency: 'usd', interval: 'year' }],
    },
  ],
  customers: [{ id: 'cus-1', email: '[email protected]', name: 'Ada Lovelace' }],
  subscriptions: [
    {
      id: 'sub-1',
      customerId: 'cus-1',
      productId: 'pro-monthly',
      status: 'active',
      amount: 2900,
      currency: 'usd',
      interval: 'month',
      currentPeriodEnd: new Date('2026-09-01'),
    },
  ],
});

Simulating a provider and its capabilities

By default the provider reports itself as testing with every capability enabled. A second options argument lets you simulate a specific provider’s identity and its capability gaps, so code that branches on client.capabilities is testable:

const provider = createInMemoryProvider(
  {},
  {
    name: 'paddle',
    capabilities: { hostedCheckout: false, checkoutSuccessUrl: false, revoke: false },
  },
);
const client = createClient({ provider });

client.providerName; // 'paddle'
await client.subscriptions.revoke({ id: 'sub-1' }); // throws RevenueError { code: 'unsupported' }
Option Type Default Description
name ProviderName 'testing' Simulate a specific provider’s identity. Also tags the pagination cursors.
capabilities Partial<RevenueCapabilities> Merged over the full-featured defaults — override only what your test needs.

Check the capability matrix for the values each real provider reports.

Asserting against state

The provider exposes its data on provider.state, so you can assert on the effects of your code:

import { expect, test } from 'vitest';
import { createClient } from 'revenue-sdk';
import { createInMemoryProvider } from 'revenue-sdk/testing';

test('cancelling schedules the end of the period', async () => {
  const provider = createInMemoryProvider({
    customers: [{ id: 'cus-1', email: '[email protected]' }],
    subscriptions: [
      { id: 'sub-1', customerId: 'cus-1', currentPeriodEnd: new Date('2026-09-01') },
    ],
  });
  const client = createClient({ provider });

  const subscription = await client.subscriptions.cancel({ id: 'sub-1' });

  expect(subscription.status).toBe('active');
  expect(subscription.cancelAtPeriodEnd).toBe(true);
  expect(subscription.endsAt).toEqual(new Date('2026-09-01'));
  expect(provider.state.subscriptions[0]!.cancelAtPeriodEnd).toBe(true);
});

test('creating a checkout records it', async () => {
  const provider = createInMemoryProvider({ products: [{ id: 'pro' }] });
  const client = createClient({ provider });

  const checkout = await client.checkouts.create({
    items: [{ product: 'pro' }],
    customerEmail: '[email protected]',
    metadata: { userId: 'user_123' },
  });

  expect(checkout.status).toBe('open');
  expect(provider.state.checkouts).toHaveLength(1);
  expect(provider.state.checkouts[0]!.metadata).toEqual({ userId: 'user_123' });
});

provider.state is { products: Product[], customers: Customer[], subscriptions: Subscription[], checkouts: Checkout[] }. Mutating operations write straight into it, so the seeded objects and the returned ones are the same instances.

Behavior of the mutating operations

Call Effect on state
checkouts.create Appends a Checkout with a generated ID, status: 'open', and a fake URL.
subscriptions.cancel Sets cancelAtPeriodEnd: true and endsAt = currentPeriodEnd. Status unchanged.
subscriptions.uncancel Clears cancelAtPeriodEnd and endsAt.
subscriptions.changePlan Sets productId to params.product, and quantity when given.
subscriptions.endTrial Sets status: 'active' and clears trialEndsAt.
subscriptions.revoke Sets status: 'canceled', clears cancelAtPeriodEnd, and sets endedAt.
customerPortal.createSession Returns a fake portal URL, or throws not_found for an unknown customer.

Anything looked up by an ID that isn’t seeded throws RevenueError with code not_found.

Pagination is exercised

The in-memory provider uses a deliberately small page size of 2 and ignores limit, so a seed with three or more items returns a cursor and your cursor-following code runs in tests rather than always fitting on one page:

const provider = createInMemoryProvider({
  products: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
});
const client = createClient({ provider });

const first = await client.products.list();
first.items.length; // 2
first.cursor; // defined — there's a third product

let count = 0;
for await (const _ of client.products.listAll()) count++;
count; // 3

Cursors are tagged with the simulated provider name, so they behave exactly like real ones — including rejecting a cursor from a different provider.

Last updated on August 6, 2026

Was this page helpful?