Integration
Testing Patterns
Patterns for unit tests, integration tests, and CI/CD pipelines using Ghostbox sandboxes.
Unit test setup
Override the base URL in your test setup to point at Ghostbox. Your application code under test makes real HTTP requests -- but they go to the sandbox instead of the live API.
test/setup.ts (Vitest)typescript
import { beforeAll } from 'vitest';
beforeAll(() => {
// Point all API clients at Ghostbox
process.env.STRIPE_API_KEY = process.env.GHOSTBOX_API_KEY;
process.env.STRIPE_BASE_URL = 'https://ghostbox.dev/sandbox/stripe';
process.env.RESEND_API_KEY = process.env.GHOSTBOX_API_KEY;
process.env.RESEND_BASE_URL = 'https://ghostbox.dev/sandbox/resend';
});test/stripe.test.tstypescript
import { describe, it, expect } from 'vitest';
import { createCustomer, getCustomer } from '../src/stripe-client';
describe('Stripe integration', () => {
it('creates and retrieves a customer', async () => {
const customer = await createCustomer({
email: 'test@example.com',
name: 'Test User',
});
expect(customer.id).toMatch(/^cus_/);
expect(customer.email).toBe('test@example.com');
const retrieved = await getCustomer(customer.id);
expect(retrieved.id).toBe(customer.id);
});
});Integration test patterns
Ghostbox sandboxes are stateful, so you can test full CRUD lifecycles and cross-resource relationships.
Full lifecycle testtypescript
import { describe, it, expect } from 'vitest';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.GHOSTBOX_API_KEY!, {
host: 'ghostbox.dev/sandbox/stripe',
protocol: 'https',
});
describe('Payment flow', () => {
it('completes a full payment lifecycle', async () => {
// 1. Create a customer
const customer = await stripe.customers.create({
email: 'buyer@example.com',
});
// 2. Create a product and price
const product = await stripe.products.create({
name: 'Widget',
});
const price = await stripe.prices.create({
currency: 'usd',
product: product.id,
unit_amount: 1500,
});
// 3. Create a payment intent
const pi = await stripe.paymentIntents.create({
amount: 1500,
currency: 'usd',
customer: customer.id,
});
expect(pi.status).toBe('requires_payment_method');
// 4. Confirm the payment
const confirmed = await stripe.paymentIntents.confirm(pi.id, {
payment_method: '4242424242424242',
});
expect(confirmed.status).toBe('succeeded');
expect(confirmed.amount_received).toBe(1500);
});
});Error path testing
Use error injection headers and magic values to test how your code handles failures. See the Error Simulation page for the full list of options.
Testing error handlingtypescript
import { describe, it, expect } from 'vitest';
describe('Error handling', () => {
it('handles rate limit errors gracefully', async () => {
const response = await fetch(
'https://ghostbox.dev/sandbox/stripe/v1/customers',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.GHOSTBOX_API_KEY}`,
'X-Sandbox-Error': 'rate_limit',
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'email=test@example.com',
},
);
expect(response.status).toBe(429);
const body = await response.json();
expect(body.error.code).toBe('rate_limit');
});
it('handles declined cards', async () => {
const response = await fetch(
'https://ghostbox.dev/sandbox/stripe/v1/charges',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.GHOSTBOX_API_KEY}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'amount=2000¤cy=usd&source[number]=4000000000000002',
},
);
expect(response.status).toBe(402);
const body = await response.json();
expect(body.error.code).toBe('card_declined');
});
it('handles bounced emails', async () => {
const response = await fetch(
'https://ghostbox.dev/sandbox/resend/emails',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.GHOSTBOX_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'hello@example.com',
to: 'bounce@resend.dev',
subject: 'Test bounce',
html: '<p>test</p>',
}),
},
);
const { id } = await response.json();
// Retrieve the email to check the bounce status
const emailResponse = await fetch(
`https://ghostbox.dev/sandbox/resend/emails/${id}`,
{
headers: {
'Authorization': `Bearer ${process.env.GHOSTBOX_API_KEY}`,
},
},
);
const email = await emailResponse.json();
expect(email.last_event).toBe('bounced');
});
});CI/CD configuration
Add your Ghostbox API key as a secret in your CI environment and set the base URL environment variables.
GitHub Actionsyaml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
env:
GHOSTBOX_API_KEY: ${{ secrets.GHOSTBOX_API_KEY }}
STRIPE_BASE_URL: https://ghostbox.dev/sandbox/stripe
RESEND_BASE_URL: https://ghostbox.dev/sandbox/resend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm testGitLab CIyaml
test:
image: node:22
variables:
GHOSTBOX_API_KEY: $GHOSTBOX_API_KEY
STRIPE_BASE_URL: https://ghostbox.dev/sandbox/stripe
RESEND_BASE_URL: https://ghostbox.dev/sandbox/resend
script:
- npm ci
- npm testState isolation guarantees
Every API key gets its own isolated state per service. This is critical for testing:
- Parallel test suites -- multiple CI jobs can run simultaneously with different keys without data conflicts.
- Clean state per run -- create a new key per CI run to guarantee a fresh starting state.
- No cross-service leakage -- creating a Stripe customer does not affect the Resend sandbox.
- Deterministic responses -- same input always produces the same output (except for generated IDs and timestamps).