Testing Travel Booking Flows Without Booking Real Flights
Our strategy for end-to-end testing of the most critical and most expensive user flow — sandbox payments, mock providers, and chaos testing for failures.

"We need to test the booking flow." "OK, book a real flight?" "That costs $400." This conversation happened during our second week. Someone actually suggested a budget line item for test bookings. The math killed it immediately: if every engineer runs the booking flow five times a day during development, we'd burn thousands of dollars a week on flights nobody would take.
Travel booking is the hardest feature to test because the happy path costs real money. You can't just click "Book" against a production flight API and hope for the best. But you also can't skip testing the most critical flow in your product. The booking path is where money changes hands, seats get reserved, and travelers commit to plans. If it breaks, the consequences are immediate and expensive.
We needed a testing strategy that gives us confidence in the booking flow without ever reserving a real seat.
Sandbox payment processing

The payment processor's test mode is the foundation of our booking test strategy. Every payment scenario we care about has a corresponding test card number or test token that triggers that specific behavior.
Successful payment. Declined card. Insufficient funds. 3D Secure challenge required. Network timeout during capture. Partial refund. Each scenario has a deterministic trigger in the sandbox. We don't need to hope a card gets declined to test the declined-card path. We use the specific test card number that always declines.
Our end-to-end booking tests create real payment intents against the sandbox. The payment processing code doesn't know it's running in test mode. The same webhook handlers fire. The same status transitions happen. The same confirmation logic runs. The only difference is that no money actually moves.
This is important: the sandbox payment path exercises the same code as production. We don't have a separate "test payment" code path. The environment variable determines which API keys are used, and the test keys route to the sandbox automatically. If the production payment code has a bug, the sandbox tests catch it.
We run payment tests for every scenario in our failure matrix: successful first attempt, successful after 3D Secure, declined and retried with different card, timeout during authorization, timeout during capture (the scariest one, because money might have moved), and webhook delivery failure with retry.
Mock travel data providers
Flight and hotel search hits external APIs that return live data. Live data is great for realism but terrible for deterministic testing. Flight prices change. Availability shifts. Routes appear and disappear. A test that passed yesterday might fail today because the $342 flight to Barcelona no longer exists.
For our integration and end-to-end tests, we use the travel data provider's sandbox environment. Sandbox searches return real route structures with stable test data. The flights are plausible (real airlines, real routes, real aircraft types) but the specific offerings are fixed. A search for London to Tokyo always returns the same set of options.
This determinism is essential. Our tests assert on specific behaviors: "When the agent finds flights with a 2-hour layover, it should flag the tight connection." If the search results change between test runs, the test is useless.
For unit tests that exercise specific code paths, we go further and use mock responses. A mock flight search response is a JSON fixture that never changes. The test loads the fixture, passes it through the ranking logic, and asserts on the output. No network calls. No API rate limits. Sub-millisecond execution.
The testing hierarchy looks like this: unit tests use mock fixtures, integration tests use the sandbox API, and end-to-end tests use the sandbox API with sandbox payments. Each layer adds realism and cost. Each layer runs less frequently.
Testing the AI agent

The AI agent is the most unpredictable component in our stack. Given the same user message, it might produce slightly different tool call sequences, different response phrasing, or different reasoning paths. Traditional assertion-based testing doesn't work well here.
We use evaluation datasets instead. An eval dataset is a set of inputs (user messages with context) and expected outcomes (not exact outputs, but behavioral expectations). "When the user asks for flights to Tokyo, the agent should call the flight search tool with Tokyo as the destination." "When the user provides passport details, the agent should not echo them back in the response."
Our eval suites cover flight searches, hotel searches, booking flows, and safety scenarios. The safety evals are particularly important: they verify that the agent doesn't leak sensitive data, doesn't hallucinate booking confirmations, and doesn't proceed with a booking without explicit user consent.
We run evals on every deployment. If an eval score drops below the threshold, the deployment pauses. This catches AI behavior regressions before they reach travelers.
The test configuration reflects the reality of testing against external APIs: 60-second timeouts (because AI inference plus tool calls can take time), 2 retries per test (because external APIs occasionally hiccup), and sequential execution (because parallel test runs would hit rate limits).
Chaos testing for failures
The happy path is easy. The failure paths are where booking systems actually break. We inject failures at every point in the booking flow to verify that the system handles them correctly.
Payment failures: authorization timeout, capture timeout, webhook delivery failure, double-charge detection, refund failure after booking cancellation.
Travel provider failures: booking confirmation timeout, seat already taken (race condition), price changed between search and booking, provider returns an error after payment succeeded.
Infrastructure failures: database connection lost during booking write, cache unavailable during session lookup, queue unavailable for confirmation email delivery.
Each failure scenario has an expected system behavior. Payment timeout during capture should trigger a reconciliation check, not a second capture attempt. Provider error after payment should initiate an automatic refund, not leave the payment captured without a booking. Database failure during booking write should roll back the transaction and return a clear error, not leave a partial booking record.
We simulate these failures using test-mode flags that trigger specific error conditions. In our test environment, we can force a payment capture to timeout, force a booking confirmation to fail, or force a database write to error. The booking code doesn't have special test logic. The failures happen at the infrastructure layer, same as they would in production.
Test data management
Test environments accumulate garbage. Every test run creates users, trips, bookings, payment intents, and provider records. Without cleanup, the test environment becomes a landfill of stale data that slows queries and confuses developers.
We clean up after every test suite run. Each test creates its data, runs its assertions, and deletes its data. For tests that can't clean up (because the test verifies that a webhook creates a record asynchronously), we run a nightly cleanup job that purges test data older than 24 hours.
Seed data is separate from test data. Seed data (airports, airlines, reference tables) is loaded once and never cleaned up. Test data (users, trips, bookings) is created per test run and always cleaned up.
The visual database browser helps here. When a test fails and leaves orphaned data, developers can inspect the database directly to understand what state the test left behind. This is faster than writing diagnostic queries.
Design your booking test strategy
If you're building a high-stakes transaction flow, here's the test pyramid that works for us.
Unit tests at the base. Fast, deterministic, mock everything external. Test business logic in isolation: pricing calculations, validation rules, state machine transitions. These run on every commit.
Integration tests in the middle. Use sandbox APIs for external dependencies. Test that your code correctly calls external services and handles their responses. These run on every pull request.
AI evaluation datasets alongside integration tests. Behavioral tests for the AI agent that verify it makes correct decisions. These run on every deployment.
End-to-end sandbox tests near the top. Full booking flow against sandbox payment and sandbox travel provider. These run before production deployment.
Chaos tests at the top. Failure injection across the booking flow. These run weekly and before major releases.
The pyramid gets more expensive and slower as you go up, but each layer catches different categories of bugs. Unit tests catch logic errors. Integration tests catch API contract mismatches. Evals catch AI behavior regressions. E2E tests catch flow-level bugs. Chaos tests catch resilience gaps.
Skip any layer and you'll find out about those bugs in production, where they cost real money and affect real travelers.
Nowah is an AI travel agent that searches and books real flights and hotels through conversation — no filters, no thirty open tabs. Plan your next trip.