PaperTiger - a stateful mock Stripe server for Elixir testing

We just published PaperTiger, a mock Stripe server we built to solve real pain points in our test suite.

The Problem

Testing Stripe integrations in Elixir (and in any language, really!) has some rough edges:

  • Stripe’s official stripe-mock is stateless - you can’t create a customer and then fetch it, which makes testing realistic flows impossible

  • Stripe’s webhook sandbox is limited - 5 endpoints max, dashboard-only management, no programmatic control

  • Shared test mode causes collisions - CI runs interfere with manual testing, leading to flaky tests

  • No time control - testing subscription renewals means waiting or hacking around Stripe’s billing cycle

We needed something that actually behaves like Stripe - stateful resources, proper webhook chains, and time control for billing cycles.

What PaperTiger Does

  • Stateful API - Create a customer, attach a payment method, subscribe them, and it all persists (in ETS)

  • Automatic webhook delivery - Resource changes emit properly-signed webhooks with retry logic

  • Time control - Accelerate time for subscription testing, or use manual mode for precise control

  • BillingEngine - Simulates subscription lifecycle: period rollovers, invoice creation, payment processing

  • Chaos mode - Inject payment failures with configurable decline codes for testing error paths

  • Contract testing - Run the same tests against PaperTiger and real Stripe to verify behavior matches

Quick Example

setup do
  {:ok, config: PaperTiger.stripity_stripe_config()}
end

test "subscription billing cycle", %{config: config} do
  {:ok, customer} = Stripe.Customer.create(%{email: "test@example.com"}, config)
  {:ok, sub} = Stripe.Subscription.create(%{customer: customer.id, ...}, config)
  
  assert sub.status == "active"
  
  # Advance time past the billing period
  PaperTiger.Clock.advance_days(32)
  
  # Subscription renewed, invoice created and paid
  {:ok, invoices} = Stripe.Invoice.list(%{customer: customer.id}, config)
  assert length(invoices.data) == 2
end

Links

We’d welcome feedback, issues, or contributions. If you’ve dealt with similar Stripe testing headaches, hopefully this helps.

10 Likes