example-petstore.com

Example domain · Not a live service · Browser visits show this page · API requests get 410 Gone

Guide · Testing

Test against a mock API, not a placeholder

Code that points at a made-up address such as api.example-petstore.com still sends real requests, and they reach whoever owns that name. A mock API gives the same convenience without that risk: it runs on your own machine or in your tests, answers instantly, and always returns the data you expect.

Why not a placeholder

  • A placeholder that looks like a real domain may be registered by someone else. Every request, including headers with keys or tokens, then reaches their server.
  • Unused names under example.com, such as api.example.com, do not resolve at all, so the code fails with a timeout or DNS error instead of showing how it handles real answers.
  • Tests that depend on a public server are slow and flaky, and they break when that server changes.

A mock API solves all three: it listens on localhost or inside the test process, and nothing leaves your machine.

Which tool when

ToolBest forRuns as
PrismYou have an OpenAPI description and want answers that follow itLocal server (Node.js or Docker), port 4010
WireMockExact, recorded answers, error cases and delays for any languageLocal server (Java or Docker), port 8080
MSWJavaScript and TypeScript: front-end development and unit testsInside the browser or the Node.js test process
json-serverA working REST API from one JSON file, for prototypesLocal server (Node.js), port 3000

For the Swagger Petstore sample API itself, run the official image locally; see Looking for the Swagger Petstore?

Prism: from an OpenAPI file

Prism reads an OpenAPI (or Swagger 2.0) description and answers every operation in it with the examples or schemas from that file. Requests that do not match the description get a clear validation error, which makes it useful for checking a client before the real API exists.

# With Node.js
npm install -g @stoplight/prism-cli
prism mock openapi.yaml

# With Docker
docker run --init --rm -v "$(pwd)":/tmp -p 4010:4010 stoplight/prism:5 mock -h 0.0.0.0 /tmp/openapi.yaml

# Then
curl http://127.0.0.1:4010/pets
curl http://127.0.0.1:4010/pets/1 -H "Prefer: code=404"

The Prefer header chooses a specific response from the description, such as an error code or a named example. With --dynamic (-d) Prism generates fresh data from the schemas on every request.

WireMock: recorded answers

WireMock answers from stub files that you write or record. It suits any language, and it can simulate slow responses and failures that a real service rarely produces on demand.

# mocks/mappings/pet.json
{
  "request":  { "method": "GET", "url": "/v1/pets/1" },
  "response": {
    "status": 200,
    "headers": { "Content-Type": "application/json" },
    "jsonBody": { "id": 1, "name": "Rex", "status": "available" }
  }
}

# Start it with the folder that holds mappings/ (and __files/ for larger bodies)
docker run -it --rm -p 8080:8080 -v "$(pwd)/mocks":/home/wiremock wiremock/wiremock

curl http://localhost:8080/v1/pets/1

Add "fixedDelayMilliseconds": 3000 to a response to test timeouts, or a status such as 503 to test retries. The admin API at /__admin lets tests add stubs and check which requests arrived.

MSW and PHP mocks: inside your tests

Mock Service Worker intercepts requests inside the application itself: in the browser through a service worker, in Node.js inside the test process. The code under test keeps calling its normal base URL; no extra server runs.

// handlers.js
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('https://api.example.com/v1/pets/:id', ({ params }) =>
    HttpResponse.json({ id: Number(params.id), name: 'Rex', status: 'available' })),
  http.post('https://api.example.com/v1/pets', () =>
    HttpResponse.json({ error: 'name is required' }, { status: 400 })),
]

// In tests (Node.js)
import { setupServer } from 'msw/node'
const server = setupServer(...handlers)
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

onUnhandledRequest: 'error' makes a test fail as soon as code calls an address without a handler, so a forgotten placeholder shows up in the test run instead of in production. In the browser, run npx msw init public/ once and start the worker from msw/browser.

In PHP tests, Guzzle’s MockHandler does the same inside the test process, and Symfony has MockHttpClient:

// PHP, Guzzle: answers in order, without a network
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;

$mock = new MockHandler([
    new Response(200, ['Content-Type' => 'application/json'], '{"id": 1, "name": "Rex", "status": "available"}'),
    new Response(400, [], '{"error": "name is required"}'),
]);
$client = new Client(['handler' => HandlerStack::create($mock), 'base_uri' => 'https://api.example.com/v1/']);

// PHP, Symfony
$client = new Symfony\Component\HttpClient\MockHttpClient(
    [new Symfony\Component\HttpClient\Response\MockResponse('{"id": 1, "name": "Rex"}')],
    'https://api.example.com/v1/'
);

The address in the handlers is api.example.com: a reserved name that never reaches a real server, even when a request slips past the mock.

json-server: a quick REST API

json-server turns one JSON file into a REST API with list, detail, create, update and delete routes. Changes are written back to the file, which makes it handy for prototypes and demos.

# db.json
{
  "pets":   [ { "id": "1", "name": "Rex", "status": "available" } ],
  "orders": []
}

npx json-server db.json

curl http://localhost:3000/pets
curl -X POST http://localhost:3000/orders -H "Content-Type: application/json" -d '{"petId": "1"}'

It has no validation or authentication, so keep it to prototypes; for contract tests, Prism or WireMock fit better.

Switching to the real API

Keep the base URL in configuration, with the mock as the value for development and tests, and the real address only where the application actually runs:

# .env.development
API_BASE_URL=http://localhost:4010

# .env.test
API_BASE_URL=http://localhost:8080

# production: set in the hosting environment, never in the repository
API_BASE_URL=https://api.your-real-service.com

More on this, including a check that stops example addresses from reaching production: Configuring API clients and SDKs.

Sources