Inspectable
Declarative scenarios are data. You can see exactly what response will be returned—no tracing through conditionals.
Scenarist is built on a core testing principle: test behavior, not implementation. This philosophy shapes everything about how you write scenarios and structure your tests.
Traditional testing often focuses on implementation details—mocking internal functions, spying on method calls, verifying that specific code paths execute. This creates fragile tests that break whenever you refactor, even when the external behavior stays exactly the same.
Behavior-focused testing asks a different question: “What does the user experience?”
// Tests are coupled to internal structureit('should call stripe.charges.create with correct params', () => { const stripeMock = jest.spyOn(stripe.charges, 'create');
await processPayment({ amount: 100 });
expect(stripeMock).toHaveBeenCalledWith({ amount: 10000, currency: 'usd', source: expect.any(String), });});Problems:
stripe.charges.create to stripe.paymentIntents.create// Tests describe user-visible outcomesit('displays success message after valid payment', async ({ page, switchScenario }) => { await switchScenario(page, 'payment-success');
await page.goto('/checkout'); await page.fill('[name="card"]', '4242424242424242'); await page.click('button[type="submit"]');
await expect(page.locator('.success')).toContainText('Payment complete');});Benefits:
The shift from implementation to behavior testing changes how you think:
| Instead of asking… | Ask… |
|---|---|
| ”Did the Stripe SDK get called?" | "Can users complete a purchase?" |
| "Was the auth token validated?" | "Are unauthorized users blocked?" |
| "Did the email service return 200?" | "Does the user see a confirmation?” |
Your tests become documentation of expected behavior, not a specification of internal mechanisms.
Scenarist enforces declarative scenario definitions—you describe what should happen, not how to make it happen. This isn’t an arbitrary constraint; it’s fundamental to maintainable testing.
// ✅ Declarative - describes what response to returnconst paymentSuccessMock = { method: 'POST', url: 'https://api.stripe.com/v1/charges', response: { status: 200, body: { id: 'ch_123', status: 'succeeded' }, },};
// ❌ Imperative - describes how to generate responseserver.use('/api/charges', (req, res) => { const amount = req.body.amount; if (amount > 10000) { return res.status(402).json({ error: 'amount_too_large' }); } return res.status(200).json({ id: generateId(), status: 'succeeded' });});Inspectable
Declarative scenarios are data. You can see exactly what response will be returned—no tracing through conditionals.
Composable
Add request matching, sequences, or state capture without rewriting procedural logic.
Scenarist deliberately prevents functions in scenarios. When you’re tempted to write if (req.something), the constraint forces you to ask: “What pattern am I actually trying to express?”
| System | Constraint | What It Forces |
|---|---|---|
| SQL | No procedural loops | Think in set operations |
| React | No imperative DOM updates | Think in component composition |
| Scenarist | No functions in scenarios | Think in match/sequence/state patterns |
The answer is usually one of:
These patterns are explicit, composable, and debuggable.
When writing tests, ask:
Don’t think of definitions as “mocking Stripe”—think of them as describing scenarios:
// ❌ Thinking in mocksconst stripeMock = { /* ... */ };
// ✅ Thinking in scenariosconst scenarios = { 'checkout-success': { /* describes what happens */ }, 'card-declined': { /* describes what happens */ }, 'stripe-timeout': { /* describes what happens */ },} as const satisfies ScenaristScenarios;Scenario names describe business outcomes, not technical implementations.
Each scenario should test one thing:
// ❌ Kitchen sink scenarioconst scenarios = { 'everything': { id: 'everything', mocks: [ { /* stripe success */ }, { /* auth success */ }, { /* email success */ }, ], },};
// ✅ Focused scenariosconst scenarios = { 'payment-success': { /* only stripe */ }, 'payment-declined': { /* only stripe, declined */ }, 'email-failure': { /* stripe success, email failure */ },} as const satisfies ScenaristScenarios;Focused scenarios make failures clear: when payment-declined fails, you know it’s a payment handling issue.