API Testing

API Functional Testing: Techniques, Examples and Automation

Sushant Joshi14 min read

Quick answer

API functional testing checks that each endpoint does what its contract and business rules say: the right status code, a response body that matches the schema, correct data changes and clear errors for bad input. It is functional testing, not performance or security testing, and it is built with equivalence classes, boundary values and state transitions, then automated in CI.

Reviewed by Rishi Gaurav

Share:
API functional testing diagram showing a request checked for status code, schema, business rules and state change, separate from performance and security checks

Part of the API Testing hub. Start with the pillar guide: What is API testing? Types, tools and how it works.

API functional testing checks that each API endpoint behaves the way its contract and business rules say it should: the right status code, a response body that matches the schema, the correct change to stored data, and a clear error when the input is wrong. It answers "does this API do the right thing?", not "is it fast?" or "is it safe?".

This guide is part of our complete guide to API testing. Here we focus on one layer: the functional suite most teams run on every pull request. You will see what it covers and how it differs from non-functional testing. You will also see the test design techniques that find real defects, a worked example suite for an orders API, and runnable pytest code you can adapt.

What is API functional testing?

API functional testing sends requests to an API and compares the response, and any side effects, with what the specification and the requirements expect. It treats the API as a black box: you don't care how the handler is written, only what it returns and what it changes.

A functional test has four parts:

  1. Arrange: set up the data and credentials the call needs.
  2. Act: send one request (or a short sequence).
  3. Assert: check the status, headers, body and schema.
  4. Verify the side effect: read the resource back, or check the event that was emitted.

Is API testing functional or non-functional?

Both, depending on the question the test asks. This is the most common point of confusion, and searchers ask it every way round ("api testing comes under functional testing", "is api testing non functional").

Functional API testingNon-functional API testing
QuestionDoes it return the right result?Does it return it fast, safely and reliably?
Typical checksStatus codes, schema, field values, business rules, error bodies, state changesLatency percentiles, throughput, rate limits, auth bypass, injection, resilience under failure
Pass/fail basisSpecification and requirementsSLOs, security baselines, capacity targets
When it runsEvery pull requestNightly, pre-release, or on a schedule
ExamplePOST /orders with quantity 0 returns 422 and an error naming quantityPOST /orders p95 latency stays under 300 ms at 200 requests per second

Authorization sits on the line. "A customer can't read another customer's order" is a business rule, so a basic check belongs in the functional suite. A systematic sweep for broken object-level authorization across every endpoint belongs to API security testing.

What API functional testing covers

A complete functional suite for one endpoint usually covers these areas. Use the list as a review checklist when you read a pull request that adds tests.

  • Happy path. A valid request returns the documented success code (200, 201, 204) and a body that matches the response schema.
  • Response schema. Required fields are present, types and formats are right (date-time, uuid, email), enums hold only allowed values, and no undocumented fields leak out.
  • Business rules. Totals add up, discounts apply only when they should, a cancelled order can't be shipped. This is where most real defects live.
  • Input validation. Missing required fields, wrong types, values outside minimum/maximum, strings that break pattern or maxLength. Each should return 400 or 422 with an error body your clients can parse.
  • Data changes. A POST really creates the record, a PATCH changes only the fields sent, a DELETE makes the next GET return 404.
  • Status codes and headers. Location on 201, correct Content-Type, ETag and Cache-Control where your API promises them.
  • Errors. A consistent error format, ideally RFC 9457 problem details, with no stack traces.
  • Pagination, filtering and sorting. First page, last page, empty result, page size limits, stable ordering.
  • Idempotency. Retrying a PUT, or a POST with an idempotency key, gives the same result and doesn't create duplicates.
  • State transitions. Resources with a lifecycle only allow valid moves.

For a field-by-field method of turning these into written cases, see how to write API test cases. For the wider map of test types, see types of API testing.

Test design techniques for API functional tests

Running more tests doesn't find more bugs. Choosing better inputs does. These five techniques come from classic test design and fit APIs well, because an OpenAPI schema already lists most of the partitions and limits.

Equivalence partitioning

Split each input into groups the API should treat the same way, then test one value from each group. Take quantity on POST /orders, defined as an integer from 1 to 100:

PartitionExample valueExpected
Valid (1–100)5201
Below range0422
Above range101422
Wrong type"five"400 or 422
Missing(field absent)422, error names quantity
Nullnull422

Six tests replace hundreds of arbitrary values, and each one checks a different rule.

Boundary value analysis

Bugs cluster at the edges: < written where <= was meant, off-by-one page counts, string limits counted in bytes instead of characters. For every limit, test the value on the boundary and the values just inside and outside it. For quantity that is 0, 1, 2, 99, 100 and 101. For a name with maxLength: 50, send 49, 50 and 51 characters, including a multi-byte character near the limit.

State transition testing

Many resources have a lifecycle. Draw it, then test every valid transition and the most likely invalid ones.

FromActionToExpected
pendingPOST /orders/{id}/paypaid200
paidPOST /orders/{id}/shipshipped200
pendingPOST /orders/{id}/ship(none)409, state unchanged
shippedPOST /orders/{id}/cancel(none)409
cancelledPOST /orders/{id}/pay(none)409

Generate API test cases from your spec (free)

Free, runs in your browser, no sign-up needed.

Invalid transitions are where APIs often return 200 and quietly corrupt data. Always read the resource back after an invalid call to confirm nothing changed.

Decision tables

When a result depends on several inputs together, list the combinations. A shipping fee might depend on customer tier, order total and destination. A decision table makes each rule one row, so a missing rule shows up as a missing row, not a production incident.

Pairwise combinations

A search endpoint with eight optional filters has hundreds of combinations. Pairwise testing picks a small set where every pair of parameter values appears at least once. It catches most interaction bugs at a fraction of the cost. Tools such as Microsoft's PICT generate the set for you.

Turn a spec into cases in minutes. Paste an endpoint into the free API test case generator and get positive, negative and boundary cases built from its schema. You can export them as CSV and adapt them to the techniques above.

Example: a functional test suite for an orders API

Here is a compact suite for POST /orders and GET /orders/{id}, built with the techniques above. The API accepts productId (UUID, required), quantity (integer 1–100, required) and couponCode (optional, pattern ^[A-Z0-9]{6}$).

IDTechniqueRequestExpected
F-01Happy pathvalid product, quantity 2201, Location header, body matches Order schema, status = pending
F-02Side effectGET the Location URL from F-01200, same id, quantity = 2
F-03Boundaryquantity 1201
F-04Boundaryquantity 100201
F-05Boundaryquantity 101422, error points at quantity
F-06Partitionquantity "two"400 or 422, no order created
F-07PartitionproductId missing422, error names productId
F-08Business rulevalid coupon SAVE10201, total = subtotal × 0.9
F-09Business ruleexpired coupon422, code COUPON_EXPIRED
F-10Partitioncoupon save10 (lower case)422, pattern violation
F-11Not foundGET /orders/{random-uuid}404, problem-details body
F-12Authorization rulecustomer B reads customer A's order404 or 403, no data returned
F-13Idempotencysame POST twice with one Idempotency-Keysecond call returns the first order, one record stored
F-14StatePOST /orders/{id}/ship while pending409, status still pending

Fourteen cases cover the success path, every input rule, two business rules, an authorization rule, idempotency and one lifecycle rule. That is a solid functional baseline for two operations. For more examples across other kinds of endpoints, see API testing examples.

Automating API functional tests with pytest

The suite above becomes code in about 80 lines. This example uses Python with pytest, requests and jsonschema. The same structure works in REST Assured, Playwright's request fixture or Jest with Supertest.

# test_orders.py
import os
import uuid
import pytest
import requests
from jsonschema import validate

BASE = os.environ.get("API_BASE_URL", "http://localhost:8080")
TOKEN_A = os.environ["TOKEN_CUSTOMER_A"]
TOKEN_B = os.environ["TOKEN_CUSTOMER_B"]
PRODUCT_ID = os.environ["TEST_PRODUCT_ID"]

ORDER_SCHEMA = {
    "type": "object",
    "required": ["id", "productId", "quantity", "status", "total"],
    "properties": {
        "id": {"type": "string", "format": "uuid"},
        "productId": {"type": "string"},
        "quantity": {"type": "integer", "minimum": 1, "maximum": 100},
        "status": {"enum": ["pending", "paid", "shipped", "cancelled"]},
        "total": {"type": "number", "minimum": 0},
    },
    "additionalProperties": False,
}


def auth(token):
    return {"Authorization": f"Bearer {token}"}


def create_order(quantity, token=TOKEN_A, **extra):
    body = {"productId": PRODUCT_ID, "quantity": quantity, **extra}
    return requests.post(f"{BASE}/orders", json=body, headers=auth(token), timeout=10)


def test_create_order_happy_path_and_read_back():
    r = create_order(2)
    assert r.status_code == 201
    assert "Location" in r.headers
    validate(r.json(), ORDER_SCHEMA)
    assert r.json()["status"] == "pending"

    got = requests.get(f"{BASE}{r.headers['Location']}", headers=auth(TOKEN_A), timeout=10)
    assert got.status_code == 200
    assert got.json()["id"] == r.json()["id"]
    assert got.json()["quantity"] == 2


@pytest.mark.parametrize("quantity,expected", [(1, 201), (100, 201), (0, 422), (101, 422)])
def test_quantity_boundaries(quantity, expected):
    assert create_order(quantity).status_code == expected


@pytest.mark.parametrize("bad", ["two", None, 2.5])
def test_quantity_wrong_type_creates_nothing(bad):
    r = create_order(bad)
    assert r.status_code in (400, 422)
    assert r.headers["Content-Type"].startswith("application/problem+json")


def test_other_customer_cannot_read_order():
    order_id = create_order(1).json()["id"]
    r = requests.get(f"{BASE}/orders/{order_id}", headers=auth(TOKEN_B), timeout=10)
    assert r.status_code in (403, 404)
    assert PRODUCT_ID not in r.text


def test_idempotency_key_prevents_duplicates():
    key = str(uuid.uuid4())
    headers = {**auth(TOKEN_A), "Idempotency-Key": key}
    body = {"productId": PRODUCT_ID, "quantity": 3}
    first = requests.post(f"{BASE}/orders", json=body, headers=headers, timeout=10)
    second = requests.post(f"{BASE}/orders", json=body, headers=headers, timeout=10)
    assert first.json()["id"] == second.json()["id"]


def test_cannot_ship_pending_order():
    order_id = create_order(1).json()["id"]
    r = requests.post(f"{BASE}/orders/{order_id}/ship", headers=auth(TOKEN_A), timeout=10)
    assert r.status_code == 409
    after = requests.get(f"{BASE}/orders/{order_id}", headers=auth(TOKEN_A), timeout=10)
    assert after.json()["status"] == "pending"

Free ZIP: 5 CSV packs + guide

API Test Case Library

159 ready-made API test cases in five CSV packs: login, pagination, search and filter, auth (API key, JWT, OAuth 2.0) and errors. Copy, adapt, run.

Download Free

A few habits make this suite reliable:

  • Read secrets and URLs from the environment, never from the file. The same tests then run locally, in CI and against staging.
  • Create the data each test needs. Tests that depend on a shared "order 42" break as soon as someone else touches it. Our guide to API test data generation covers factories and cleanup.
  • Assert the side effect, not just the status. F-02 and the read-back in the state test catch the bugs a status-only check misses.
  • Keep one behaviour per test. When test_quantity_boundaries[101-422] fails, the name already tells you what broke.

For a fuller pytest setup with fixtures and reporting, see API testing with Python and pytest.

Generating the baseline from the spec

Hand-writing every partition and boundary for 200 endpoints takes weeks. The schema already contains required, type, format, pattern, enum, minimum/maximum and minLength/maxLength, so a generator can plan those cases for you. Shift-Left API does this with its schema-aware test generation: it reads each constraint and plans a boundary or negative test for it. Wrong-type tests send values the API can't silently coerce. It works without an AI key; AI, when configured, adds edge cases on top. You still write the business-rule and state tests yourself, because no spec captures those fully.

Running functional tests in CI

Functional API tests are the right gate for pull requests: they need no browser, finish in seconds to minutes, and fail for a reason a developer can act on.

  1. Run on every pull request against an ephemeral or shared test environment, and block the merge on failures.
  2. Split by speed. Keep a smoke subset (one happy path per resource) under two minutes, and run the full suite after it.
  3. Publish JUnit XML so failures show up inline in GitHub Actions, GitLab or Azure DevOps.
  4. Quarantine flaky tests quickly, with an owner and a date. A red build people ignore is worse than no build.
  5. Track coverage by endpoint and by rule, not by line count. See how to measure API test coverage.

When you need a step-by-step pipeline setup, follow API test automation with CI/CD. Once the suite is stable, the same tests become your API regression suite.

Common API functional testing mistakes

  • Checking only the status code. A 200 with an empty array, a wrong total or a missing field still passes. Validate the schema and the key values.
  • Testing only the happy path. Most production incidents come from inputs nobody tried. Aim for at least as many negative and boundary tests as positive ones.
  • Shared, mutable test data. Tests that pass alone and fail together almost always share data. Create and clean up per test.
  • Asserting on volatile fields. Comparing whole bodies, including timestamps and generated IDs, makes tests fail for no reason. Assert on what matters and on types for the rest.
  • Mixing functional and performance goals. A functional test with a 200 ms timeout fails on a slow CI runner and teaches people to ignore red builds. Keep latency targets in performance tests.
  • Ignoring the error contract. Clients parse your errors too. Assert the error format, the field name and the error code, not only the status.

API functional testing FAQ

Is API testing functional or non-functional?

API testing can be either. Most API testing is functional: it checks that an endpoint returns the right status code, body and side effects for a given input. Load, latency, security and reliability checks against the same API are non-functional API testing. Teams usually start with functional tests and add non-functional suites once the behaviour is stable.

What is the difference between API testing and functional testing?

Functional testing is a goal (does the system do what the requirement says) and can be done through the UI, the API or a unit test. API testing is a layer: tests that talk to the system over HTTP or another protocol. API functional testing is where the two overlap, checking behaviour directly at the API without a browser.

What should an API functional test check?

At minimum the status code, the response schema, the values of key fields, the data change the call should cause (read it back), and the error format for invalid input. Good suites also cover authorization rules that are part of the business logic, pagination, idempotency and state transitions such as an order moving from pending to paid.

Which test design techniques work best for API functional testing?

Equivalence partitioning and boundary value analysis for parameters and body fields, state transition testing for resources with a lifecycle, decision tables for rules that combine several inputs, and pairwise combinations when an endpoint has many optional parameters. The OpenAPI schema gives you most of the partitions and boundaries for free.

Can API functional testing be automated?

Yes, and it should be. API functional tests are fast, need no browser and are deterministic when test data is controlled, which makes them the best layer to run on every pull request. Write them in a code framework such as pytest, REST Assured or Playwright, or generate them from the OpenAPI spec, and gate merges on the results.

Sources and further reading

Ready to shift left with your API testing?

Import your OpenAPI spec and get a runnable test suite, in our cloud or on your own infrastructure.