Skip to content

Testing Philosophy

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 structure
it('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:

  • Breaks if you switch from stripe.charges.create to stripe.paymentIntents.create
  • Breaks if you add a wrapper function
  • Tests internal structure, not user experience

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 return
const paymentSuccessMock = {
method: 'POST',
url: 'https://api.stripe.com/v1/charges',
response: {
status: 200,
body: { id: 'ch_123', status: 'succeeded' },
},
};
// ❌ Imperative - describes how to generate response
server.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?”

SystemConstraintWhat It Forces
SQLNo procedural loopsThink in set operations
ReactNo imperative DOM updatesThink in component composition
ScenaristNo functions in scenariosThink in match/sequence/state patterns

The answer is usually one of:

  • Match criteria — Different responses based on request content
  • Sequences — Ordered progression through states
  • State capture — Values that flow between requests

These patterns are explicit, composable, and debuggable.

When writing tests, ask:

  1. What does the user see? Focus on visible outcomes, not internal state.
  2. What scenarios matter? List the external conditions that affect behavior.
  3. What could go wrong? Error cases are often more important than happy paths.

Don’t think of definitions as “mocking Stripe”—think of them as describing scenarios:

// ❌ Thinking in mocks
const stripeMock = { /* ... */ };
// ✅ Thinking in scenarios
const 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 scenario
const scenarios = {
'everything': {
id: 'everything',
mocks: [
{ /* stripe success */ },
{ /* auth success */ },
{ /* email success */ },
],
},
};
// ✅ Focused scenarios
const 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.