API Testing Approaches Compared

An API can be tested by a person sending requests, by scripts asserting on responses, by comparing every response with the published contract, by standing in for the API with a mock so its consumers can be tested, and by loading it until it slows or fails. Each approach answers a different question, and a team that uses only one — usually the first — ships with the other four questions unanswered.

This guide sets the five approaches side by side: what each catches, what it misses, what it costs, and which protocols change the rules. It ends with a mix that fits most products and the reasons to depart from it.

The five approaches

  • Exploratory requests — a person builds requests in a client (Postman, cURL, a browser tool), reads the responses and judges them. Fast to start, catches the unexpected, proves nothing after the session ends.
  • Scripted functional tests — code that sends requests and asserts on status, headers and body, run in CI. Proves the same things every time; finds only what it was written to find.
  • Contract testing — every response is checked against the API's specification (an OpenAPI document, a JSON Schema, a GraphQL schema), so a field that changes type or disappears fails the test without anyone writing an assertion for it.
  • Mocking and service virtualisation — a stand-in returns chosen responses so the API's consumers can be tested before the API exists, and so error paths (500, 429, timeouts, malformed bodies) can be produced on demand.
  • Load and performance testing — concurrent requests with a realistic data set to measure latency, throughput and error rate under load, with tools such as JMeter or k6.

Try it: API Request Builder Try it: REST API Tester Try it: API Response Validator

What each one catches — and misses

The approaches are not rivals; they cover different failure classes. Exploratory testing catches wrong business logic, confusing error messages and undocumented behaviour, and misses regressions because nothing re-runs it. Scripted tests catch regressions on the paths that were scripted and miss everything else. Contract testing catches breaking changes to the shape of the API — the class of defect that breaks a mobile app already in users' hands — and misses wrong values inside a correct shape. Mocks let consumers be tested in isolation and error paths be forced, and miss the real API's actual behaviour, which is why a contract must tie the mock to the truth. Load testing catches capacity and concurrency problems and misses functional ones entirely.

  • Wrong business logic in a new endpoint → exploratory, then scripted once known.
  • A field renamed or retyped in a release → contract testing.
  • A consumer that mishandles a 429 or a timeout → mocking.
  • A query that is fine for one user and fails at a hundred → load testing.
  • A regression in something that worked last month → scripted tests in CI.
The approaches are not rivals: each one catches a class of failure that another misses, so no single approach covers them all.
ApproachCatcheswith an example failureMisses
Exploratory testingWrong business logic, confusing error messages, undocumented behaviourWrong business logic in a new endpoint (scripted once known)Regressions, because nothing re-runs it
Scripted testsRegressions on the paths that were scriptedA regression in something that worked last month, caught in CIEverything else
Contract testingBreaking changes to the shape of the APIA field renamed or retyped in a releaseWrong values inside a correct shape
MockingConsumer bugs, with consumers tested in isolation and error paths forcedA consumer that mishandles a 429 or a timeoutThe real API’s actual behaviour, so a contract must tie the mock to the truth
Load testingCapacity and concurrency problemsA query that is fine for one user and fails at a hundredFunctional problems, entirely

Contract testing: the specification as the oracle

An API contract is a machine-readable description of every operation: paths and methods, parameters, request and response schemas, status codes, authentication. OpenAPI is the common format for HTTP APIs; GraphQL carries its own schema; SOAP has WSDL. With a contract in place, a test does not need hand-written assertions for each field — a validator checks that the response matches the schema for that status code, and any drift is a failure.

Contract testing works in two directions. Provider-side, the API's responses are validated against its own specification, catching the case where the implementation drifted from the document. Consumer-side (consumer-driven contracts), each consumer records the subset of the API it relies on, and the provider is tested against every consumer's expectations before a change ships. The first is cheap and catches most breaks; the second is what a platform with many clients grows into.

  • Validate the specification itself first — an invalid OpenAPI document produces meaningless test results.
  • Test the error responses against the contract too; 4xx and 5xx bodies drift more often than 200s.
  • Generate the first test cases from the specification, then add the business-rule cases a generator cannot know.

Try it: OpenAPI Validator Try it: API Contract Validator Try it: Response Contract Comparator Try it: OpenAPI → Test Cases

Mocking: testing the consumer and the error paths

A mock is a stand-in for a real API that returns responses you choose. It is how a front-end or a dependent service is tested before the real API is built, how a third-party service that cannot be called in test (a payment gateway, a government registry) is replaced, and — most valuable — how error paths are produced at will: a 503 with a Retry-After header, a 429, a response that arrives after 30 seconds, a body that is valid JSON but the wrong shape.

The risk of a mock is that it agrees with the consumer and not with reality. Two habits contain it: generate the mock from the same contract the provider is tested against, and run at least one test against the real API in a staging environment before release.

Try it: Mock API Response Generator Try it: Mock Error Payload Generator Try it: API Status Code Simulator

REST, GraphQL, SOAP and webhooks: what changes

  • REST over HTTP — status codes carry meaning (RFC 9110), so a test asserts on them; resources have URLs, so caching and idempotency can be tested per method; errors should follow a documented shape, ideally the Problem Details format of RFC 9457.
  • GraphQL — one endpoint, usually one status code (200) even for errors, which live in an errors array in the body. Tests must assert on the body, not the status; a schema is always available for contract testing; and query depth and complexity need load tests because a single request can be arbitrarily expensive.
  • SOAP — XML envelopes described by a WSDL; validation against the XSD is the contract test; faults are structured; tooling is older but the contract discipline is the strictest of the four.
  • Webhooks — the API calls you, so testing needs a receiver that records what arrived, checks the signature (usually an HMAC over the body with a shared secret), and verifies retries and ordering. The consumer must also be tested for duplicate deliveries, because at-least-once is the norm.

Try it: GraphQL Query Tester Try it: SOAP Request Tester Try it: Webhook Tester Try it: Webhook Signature Analyzer

Choosing a mix

For most products the mix is: a contract-validated scripted suite at the API level running in CI (the backbone), exploratory sessions on every new or changed endpoint, mocks for every external dependency and for the error paths, and a load test with real numbers before launch and after any change to a hot path. Exploratory requests alone — the way most API testing starts in practice — should be one part of that mix, not the whole of it.

Depart from the default when the API is a public product (add consumer-driven contracts and versioning tests), when it fronts a regulated process (add security tests from the OWASP API Security Top 10, especially object-level authorisation), or when it is internal and changes daily (lean harder on contract tests and less on hand-written assertions, because they survive change).

Try it: Concurrent Request Test Generator Try it: API Authentication Tester

Frequently asked questions

Is Postman enough for API testing?

Postman and similar clients are excellent for exploratory requests and can run saved collections with assertions in CI, which covers the first two approaches. They do not by themselves give contract validation against a specification, mocking of error paths at scale, or load testing at realistic concurrency; those need the specification, a mock layer and a load tool alongside.

What is the difference between contract testing and integration testing?

Integration testing calls the real API and checks real behaviour end to end, including data and side effects. Contract testing checks only that requests and responses match the agreed shape, and can run without the real dependency. Contract tests are faster and more stable; integration tests catch what contracts cannot — wrong values, wrong data, wrong side effects. Most suites need both, with far more of the first.

Do we still need mocks if we have a staging environment?

Yes, for two reasons. Staging cannot produce error paths on demand — you cannot ask a real payment gateway to time out at the moment your test needs it — and staging dependencies are shared and slow, which makes tests flaky. Mocks give deterministic, fast tests; staging gives one final confirmation against reality.

How should API errors be tested?

Every documented error response should have a test: the status code, the body shape (ideally RFC 9457 Problem Details), and that no sensitive detail leaks — no stack traces, no SQL, no internal hostnames. Then the undocumented ones: malformed JSON, wrong content type, oversized bodies, missing authentication, expired tokens. The error paths are where most security findings live.

Which approach should a team start with?

A contract. Writing or validating the OpenAPI document first gives every other approach something to test against, generates the first scripted cases and the mocks, and forces the design questions — status codes, error shapes, pagination — to be answered before they are implemented three different ways.

References

  • RFC 9110: HTTP SemanticsIETF · 2022The meaning of HTTP methods, header fields, status codes and content negotiation, independent of the wire version.
  • OpenAPI SpecificationOpenAPI InitiativeThe standard for describing REST APIs: paths, operations, parameters, schemas, responses and security schemes.
  • GraphQL SpecificationGraphQL FoundationThe GraphQL query language and type system, validation rules and execution semantics.
  • RFC 9457: Problem Details for HTTP APIsIETF · 2023The application/problem+json error format (type, title, status, detail, instance) for machine-readable API errors.
  • Postman DocsPostmanOfficial documentation for the API client used to send requests, write test scripts and run collections.
  • OWASP API Security Top 10 (2023)OWASP Foundation · 2023The ten most common API weaknesses: broken object-level authorization, broken authentication, unrestricted resource consumption and more.
  • Apache JMeter User's ManualApache Software FoundationHow to build, parameterise and run load tests with JMeter, and how to read its listeners and reports.

Practice exercise

Choose the API testing mix for a delivery platform

Your task

You are the only tester on the API team of a fictional grocery delivery platform. Three kinds of clients call its REST API: Android and iOS apps, a dashboard for partner stores through a GraphQL endpoint, and a third-party payment gateway that sends webhooks. Eight problems got through last quarter, and a national promotion campaign starts in a little over five weeks. The material below describes who calls the API, how it is tested today, the eight problems I1 to I8 and a question from the team lead.

Work with the guide’s five approaches — exploratory requests, scripted functional tests, contract testing, mocking and load testing — and with what it says about REST, GraphQL and webhooks. Pen and paper are enough; allow 30–45 minutes. If you want to see the point of I7 for yourself, sign a short sample body with any secret in the HMAC Generator, change one character of the body, and check both versions with the guide’s Webhook Signature Analyzer.

Material
Grocery delivery platform (fictional) - REST API v2, six developers, one tester
Today: Mon 5 Oct 2026. A national promotion campaign starts on Thu 12 Nov 2026.

Who calls the API
  Android and iOS apps    customers update slowly; app versions from the last six
                          months are still in use
  Partner dashboard       the stores’ order screen; uses the GraphQL endpoint
                          POST /graphql
  Payment gateway         our API calls it to take a payment. It calls us back at
  (third party)           POST /webhooks/payments with payment.succeeded or
                          payment.failed; each event has a unique id and an
                          X-Signature header (HMAC-SHA256 of the body with a shared
                          secret). Its documentation says events are delivered at
                          least once.

How the API is tested today
  Before each release (every two weeks), the tester sends the requests of a Postman
  collection by hand and reads the responses. No API test runs in CI.
  An OpenAPI document exists; it was last updated five months ago.
  The gateway’s sandbox approves every test payment at once.

What went wrong last quarter (I7 was found in a code review, the rest in production)
I1  Release 2.14 renamed a field in GET /api/v2/cart and changed the web front end in
    the same release. Android 5.2 then showed every delivery fee as 0.00.
      2.13  {"subtotal": 84.50, "deliveryFee": 9.00, "total": 93.50}
      2.14  {"subtotal": 84.50, "delivery_fee": 9.00, "total": 93.50}
I2  The gateway answered after 40 s. Our API stopped waiting after 30 s and returned
    500, and the app showed "Payment failed". Customers paid again, but the first
    payment had gone through: they were charged twice.
I3  New in 2.15: POST /api/v2/cart/promo. The code WELCOME50 is valid once per
    customer, but it was accepted again on a customer’s second order.
I4  At the evening peak of the last campaign, GET /api/v2/orders (order history)
    took more than 20 s, then returned 504 for 25 minutes. Test customers have about
    30 orders each; long-standing customers in production have up to 2,000.
I5  After a change, the dashboard showed an empty order list to every store with more
    than 100 open orders, for two days. The dashboard’s health check looks only at
    the status code:
      HTTP/1.1 200 OK
      {"data": {"storeOrders": null},
       "errors": [{"message": "Result too large", "path": ["storeOrders"]}]}
I6  The gateway delivered the same payment.succeeded event twice (a retry). The order
    was marked paid twice and two invoices were issued.
I7  POST /webhooks/payments also accepts a request with no X-Signature header: anyone
    who knows the URL could mark an order as paid.
I8  The filter inStock=true of GET /api/v2/stores/{id}/products worked in 2.12 and
    broke in 2.13: sold-out products were listed. That release’s hand-run pass did
    not include filters.

The team lead asks
  "Before the campaign, can we keep testing in Postman by hand and just add a
  second tester?"
Steps
  1. For each problem I1 to I8, name the approach that would most likely have caught it before release, and say in one sentence why the hand-run Postman pass missed it.
  2. For the GraphQL problem and the two webhook problems, write what the test must assert or produce that a plain REST status-code check would not.
  3. For each approach you chose, name one thing it would still miss in this product, and which other part of the mix covers that gap.
  4. Propose the team’s mix: what runs on every build, for every new or changed endpoint, before each release and before the campaign; where this product departs from the guide’s default mix; and what you do first, starting today.
  5. Answer the team lead’s question in three or four sentences.
Show the model answer
Model answer

The pattern first. Today the API gets one approach — exploratory requests — and not even real exploration: the same collection replayed by hand before each release. At best that could have caught I3, I7 and, with a large enough test store, I5. Each of the other problems needed an approach the team does not use. The model answer below names, for each problem, the approach most likely to have caught it before release and why the hand-run pass missed it; then what each approach still misses, the mix, and the reply to the team lead.

I1 — contract testing, provider-side. If every response of GET /api/v2/cart is validated in CI against the OpenAPI document, the build fails the moment deliveryFee disappears, provided the schema marks deliveryFee as required; check that it does, because a schema whose fields are all optional accepts the renamed response. The hand-run pass missed it because "delivery_fee": 9.00 looks perfectly reasonable to a person, and the web front end, changed in the same release, showed no problem. The installed apps are why this team also needs consumer-driven contracts: each app version still in use records the fields it reads, and a provider change that breaks one of them fails before it ships. A field that has to be renamed is then sent under both names, or in a new API version, until the old apps are out of use.

I2 — mocking. The gateway’s sandbox approves every payment at once, so neither a hand-run request nor a staging run can produce a gateway that answers after 40 seconds. A mock of the gateway can, together with the other error paths: no answer at all, 503 with a Retry-After header, 429, and a body that is valid JSON but the wrong shape. What the API should do when the outcome of a payment is unknown must be agreed with the team — for example, report the payment as pending and ask the gateway for its status instead of reporting a failure — and the test asserts what matters to the customer: one order, one charge. The test then runs against the mock in CI on every build. Before each release, one run against the real sandbox from staging confirms that the mock still behaves like the gateway.

I3 — exploratory testing, then a scripted test. A contract test passes the second discount, because the response has exactly the right shape: this is wrong business logic in a new endpoint. A tester exploring POST /api/v2/cart/promo with a charter such as “break the once-per-customer rule” tries the code on a second order, after cancelling the first order, from two devices at the same moment and in lower case (welcome50). The hand-run pass replays a fixed collection, and a new rule is exactly what it does not contain: sending requests by hand is not the same as exploring. Each case found becomes a scripted test, so that the rule stays fixed.

I4 — load testing with a realistic data set. Hand-run requests cannot show it: one person sends one request at a time, for test customers with about 30 orders, while the failure needs many customers with up to 2,000 orders each at the same moment. Create test data with production-like order counts, agree the targets with the product owner before the test (a response time and an error rate at the expected peak), and run it early enough to fix what it finds and run it again before 12 Nov — and again after any change to order history, which is a hot path. A load test says nothing about whether the orders listed are the right ones.

I5 — scripted tests that assert on the GraphQL body. A GraphQL server usually answers 200 even when a field fails and puts the failure in the errors array, so a check of the status code — the dashboard’s health check, or a test — stays green. The test asserts that errors is absent and that data.storeOrders is a list with the expected orders, and it runs with test stores on both sides of the point where it broke: 100 and 101 open orders (2-value boundary value analysis). A contract check against the GraphQL schema alone would pass this response: storeOrders came back as null next to the error, which GraphQL does only for a field the schema declares nullable. Fix the health check the same way, and add a store of the largest realistic size to the load test, because a single GraphQL request can be very expensive.

I6 — a webhook test in which the test plays the gateway. “Delivered at least once” means duplicates are normal, not a gateway fault, so the receiver must handle them. The test sends the same signed payment.succeeded event, with the same event id, twice and asserts one paid order and one invoice; a second test sends events out of order, such as payment.failed after payment.succeeded for the same payment, and checks the outcome the team has defined (ask for it if it is not written down). The sandbox cannot be asked to retry at the moment a test needs it, so this is mocking — producing the gateway’s behaviour on demand — run as a scripted test in CI.

I7 — scripted negative tests of the webhook receiver, and a security defect. Three requests must be rejected with the error status the team documents (for example 401) and leave the order unchanged: no X-Signature header, a signature made with the wrong secret, and a correctly signed body changed after signing. The last one is what the HMAC Generator exercise shows: one changed character gives a different signature. The collection contained only valid gateway calls, so the hand-run pass never sent such a request; the code review that found it was static testing, and a scripted test keeps it fixed. Because the API moves money and holds customers’ personal data, add tests from the OWASP API Security Top 10 2023 to the mix, object-level authorisation first: one customer must not be able to read or change another customer’s cart or orders. Test only systems you are authorised to test — here, your team’s staging.

I8 — scripted functional tests in CI. It is a regression: a filter that worked in 2.12 broke in 2.13, in a release whose hand-run pass happened not to include filters. A test that inStock=true returns no product with zero stock, in a suite that runs on every build, would have failed on the change that broke it. A contract test passes it, because a sold-out product in the list has the right shape.

What each approach still misses in this product, and what covers it. Contract testing misses wrong values inside a correct shape — I3, I8 and the null of I5 — so scripted tests assert on business rules. Scripted tests find only what they were written to find, and a new rule like I3 is not among them, so every new or changed endpoint gets an exploratory session. An exploratory session proves nothing once it ends, so each finding becomes a scripted test. The gateway mock agrees with your team and not necessarily with the gateway, so it is built from the gateway’s own API documentation and checked against the real sandbox from staging before each release. A load test says nothing about functional correctness — order history can be fast and wrong — which the CI suite covers.

The mix. On every build in CI: the scripted API suite, with I1 to I8 among its first regression tests; every response, error responses included, validated against the OpenAPI document; GraphQL tests that assert on the body; the webhook tests of I6 and I7; and the gateway mocked for its error paths. For every new or changed endpoint: a short exploratory session whose findings become scripted tests. Before each release: one run against the real gateway sandbox from staging. Before the campaign, and after any change to a hot path such as order history: a load test with production-like data. That is the guide’s default mix with two departures. The installed apps cannot be updated together with the API — versions from the last six months are still in use — so for them the API behaves like a public product and needs consumer-driven contracts and tests for every app version still supported. And it moves money and personal data, so it needs the OWASP API Security Top 10 tests.

What you do first, today: the contract. Bring the five-month-old OpenAPI document up to date with the API and validate it — an outdated or invalid document makes every contract result meaningless — then generate the first scripted cases from it (the guide’s OpenAPI → Test Cases tool helps) and add the business-rule cases a generator cannot know, such as I3. With about five weeks to 12 Nov, a workable order is: the contract and the contract checks in CI; then the scripted suite with I1 to I8, the gateway mock and the webhook tests; then the load test, early enough to fix what it finds and run it again before the campaign.

The reply to the team lead: “No: more people sending requests by hand answer only one of the five questions. Of last quarter’s eight problems, hand-run requests could at best have caught I3, I7 and perhaps I5; they could not have shown I1, because a renamed field looks fine to a person, nor I2 and I6, because the sandbox cannot produce a slow answer or a retry, nor I4, because one person cannot produce the campaign peak — and nothing they find stays checked afterwards, which is how I8 happened. Postman can run saved collections with assertions in CI, which covers scripted tests, but contract validation, the gateway mock and a load test need the specification, a mock layer and a load tool alongside. A second tester would be best spent on exploratory sessions for new endpoints and on turning their findings into scripted tests.”

Check your own work. Each problem should have an approach and the reason the current practice missed it. Other mappings are fine when the reason is stated — for example I7 under exploratory testing, as long as you add that only a scripted test keeps it fixed, or I6 under scripted tests, as long as you note that the duplicate delivery must be produced on demand. Your mix should say when each approach runs (every build, every new endpoint, every release, before the campaign) and what each one misses. In ISTQB CTFL v4.0 terms: the OpenAPI document is the test basis and, for contract tests, the test oracle; exploratory testing is an experience-based test technique; the CI suite is regression testing, and the fix of each of I1 to I8 first needs confirmation testing; the load test is non-functional testing of performance efficiency (ISO/IEC 25010); and the staging run against the real gateway is system integration testing.

Your progress on this topic

Part of the QA Roadmap

Tools mentioned in this guide

Compose and send HTTP requests (method, URL, headers, auth, body) and inspect the response — a lightweight Postman in your browser.

API Testing

Test REST endpoints with full control over method, headers and JSON body, then assert on status, headers and JSON paths.

API Testing

Validate an API response against expectations: status, required headers, JSON schema and field rules.

API Testing

Validate OpenAPI 3.x and Swagger 2.0 documents for structural errors and best-practice issues.

Web

Validate that an API request/response pair conforms to its OpenAPI contract: path, method, parameters, body and response schema.

Validation

Compare an actual API response against an expected contract (example or schema) and report breaking vs non-breaking differences.

Validation

Generate a structured test-case matrix (positive, negative, auth, boundary) from an OpenAPI / Swagger specification.

Automation

Generate complete mock API responses (status, headers, JSON body) for success, list, detail, error and empty cases.

Mocking

Generate field-level validation error payloads for forms and APIs, in bulk, for UI error-state testing.

Mocking

Run GraphQL queries and mutations with variables and headers against any endpoint and inspect the response.

API Testing

Send SOAP 1.1 / 1.2 envelopes with SOAPAction headers and view the formatted XML response.

API Testing

Get a unique URL, send webhooks to it and inspect method, headers, body and timing in real time.

API Testing

Verify webhook signatures (HMAC-SHA256/SHA1, Stripe, GitHub, Shopify, Slack styles) with your secret — locally.

API Testing

Generate concurrency test scripts (k6, JMeter JMX, Locust, Artillery, Node.js) with safe defaults for a target endpoint.

Automation

Get a public endpoint that returns any HTTP status code, with optional delay and custom JSON body, to test client error handling.

API Testing

How Mutqan Soft can help

More guides

More guides