Mid-Level · Automation Technique

API Test Automation

Test the contract — status code, schema, and error body — before trusting any API response. APIs are faster, stabler, and cheaper to maintain than UI tests.

Mid-Level ISTQB CTAL-TAE v2.0 — K3 Apply ~12 min read + exercise

1 The Hook — Why This Matters

In 2021, a Wellington SaaS startup lost a major client when their payment integration silently failed. The API returned HTTP 200 OK with a body that said {"status": "error", "message": "Card declined"}. The frontend parsed the JSON, saw "error," and showed a decline message. But the automation suite only checked the status code.

For three releases, the payment health check was green in CI while real customers could not complete purchases. The QA lead found it during a manual smoke test the day before go-live. The root cause? A junior automation engineer had written assert response.status_code == 200 and called it done.

APIs are not just about whether the server responds. They are about whether the contract is honoured. Status codes, headers, schema, and error bodies all matter. API testing done well finds defects in minutes that UI tests would take hours to surface.

2 The Rule — The One-Sentence Version

Test the contract — status code, schema, headers, and error body — not just the happy path data.

An API contract is a promise. The consumer promises to send valid JSON; the provider promises a defined shape back. Your job is to verify both sides of that promise, including what happens when someone breaks it.

3 The Analogy — Think Of It Like...

Analogy

Ordering at a drive-thru.

You pull up and ask for a flat white. You do not just check that the window opened (status code). You check the receipt (headers), the cup contents (payload), and whether the lid is on (schema). If the barista hands you an empty cup with a correct receipt, you have a 200 status and a broken contract. API testing is checking the whole order, not just that the window opened.

4 Watch Me Do It — Step by Step

Here is a real example testing a New Zealand employee API. Follow these steps for every endpoint you automate.

  1. Read the contract Understand the method, required fields, headers, and expected status codes before writing a single line of test code.
  2. Write the happy path first Create a valid request and assert status, schema, and critical field values.
  3. Assert the schema, not just the data Use a schema validator or strict assertions to ensure the response shape does not drift.
  4. Test negative cases with equal rigour Malformed payloads, missing auth, and invalid IDs should return predictable errors.
  5. Verify headers and auth behaviour Content-Type, caching directives, and token expiry all affect production behaviour.
Python / requests + pytest — Happy path
def test_create_employee():
    response = requests.post(
        "https://api.payroll.co.nz/v1/employees",
        json={"first_name": "Aria", "last_name": "Chen", "ird": "123456789"},
        headers={"Authorization": f"Bearer {token}"}
    )
    assert response.status_code == 201
    data = response.json()
    assert data["id"] is not None
    assert data["first_name"] == "Aria"
    assert data["created_at"]  # schema check
Python / requests + pytest — Negative case
def test_create_employee_without_auth():
    response = requests.post(
        "https://api.payroll.co.nz/v1/employees",
        json={"first_name": "Aria", "last_name": "Chen"}
    )
    assert response.status_code == 401
    assert response.json()["error"] == "Missing authorization header"
Python / requests + jsonschema — Schema validation
from jsonschema import validate, ValidationError

def test_create_employee_schema():
    response = requests.post(
        "https://api.payroll.co.nz/v1/employees",
        json={"first_name": "Aria", "last_name": "Chen", "ird": "123456789"},
        headers={"Authorization": f"Bearer {token}"}
    )

    schema = {
        "type": "object",
        "properties": {
            "id": {"type": "string"},
            "first_name": {"type": "string"},
            "created_at": {"type": "string", "format": "date-time"},
        },
        "required": ["id", "first_name", "created_at"]
    }

    assert response.status_code == 201
    validate(instance=response.json(), schema=schema)  # validates structure
REST methods at a glance
MethodSafeIdempotentPurpose
GETYesYesRetrieve resource
POSTNoNoCreate new resource
PUTNoYesReplace entire resource
PATCHNoNoPartial update
DELETENoYesRemove resource
Pro tip: Chain dependent API calls. Create a user in test one, then use the returned id to test the GET endpoint in test two. Never hard-code IDs that only exist in your local database.

5 When to Use It / When NOT to Use It

✅ Use API testing when...

  • You need fast feedback on backend logic
  • The UI is still in development but the API is stable
  • You are testing microservices or third-party integrations
  • You want to validate business rules without browser overhead
  • You are running a pre-deployment smoke test

❌ Don't use API testing when...

  • You need to verify user-facing UI behaviour
  • Authentication flows involve redirects or cookies
  • You are doing load or performance testing (use k6 or JMeter)
  • The API is undocumented and unstable
  • You treat it as a replacement for all E2E coverage

Before you apply this technique, ask:

  • Do you have the API contract documented (method, status codes, schema)?
  • Are you testing against a stable API, or will it change during development?
  • Can you isolate this API from external dependencies (mocking third-party services)?
  • Do you need to test the UI flow, or just the business logic?

6 Common Mistakes — Don't Do This

🚫 Only checking the status code

I used to think: If the server returns 200, the API works.
Actually: A 200 with an empty body or wrong schema is a broken API. Always assert at least one field value and verify the response shape. The Wellington payment bug was a 200 with an error message in the body.

🚫 Hard-coding IDs from the dev database

I used to think: Using /users/42 in tests was fine because 42 always existed locally.
Actually: IDs change between environments, and tests that rely on specific database state are not portable. Create the resource in a setup step, capture the returned ID, and use that in downstream tests.

🚫 Ignoring error response bodies

I used to think: A 400 means the validation works; I do not need to check the message.
Actually: Error messages guide frontend developers and support staff. If the 400 for a missing email returns "Invalid input" instead of "Email is required", the frontend cannot show a helpful message. Assert error bodies with the same precision as success bodies.

When this technique fails

API testing breaks when the contract changes without documentation updates, when you test in isolation without integration, or when you assume status codes always mean the same thing across different APIs. A 500 from a third-party service is real, but a 500 from your own API means you have a bigger problem than the test can catch alone.

7 Now You Try — Interview Warm-Up

🎯 Interactive Exercise

Write the missing assertions for this API test:

def test_get_invoice():
    response = requests.get(
        "https://api.billing.co.nz/v1/invoices/INV-2024-001",
        headers={"Authorization": f"Bearer {token}"}
    )
    assert response.status_code == 200
    data = response.json()
    # TODO: assert schema and business rules

Add at least three meaningful assertions before revealing the answer.

Suggested assertions:

    assert "id" in data
    assert data["status"] in {"draft", "sent", "paid", "overdue"}
    assert data["total"] > 0
    assert data["currency"] == "NZD"
    assert data["gst_amount"] == round(data["subtotal"] * 0.15, 2)

Tip: The GST assertion catches calculation bugs that a 200 status alone would miss. Schema checks ("id" in data) catch silent field removals during refactors.

8 Self-Check — Can You Actually Do This?

Click each question to reveal the answer. If you got all three, you're ready to practice.

Q1. Why is PUT idempotent but POST is not?

Calling PUT the same multiple times produces the same result because it replaces the resource entirely. Calling POST multiple times creates multiple resources (e.g., three identical orders). That is why retry logic for POST must include deduplication keys, while PUT retries are safe.

Q2. What is the difference between 400 Bad Request and 422 Unprocessable?

400 means the server could not parse the request at all (malformed JSON, wrong Content-Type). 422 means the request was syntactically valid but semantically wrong (e.g., a negative age or a start date after an end date). Your tests should cover both separately.

Q3. Should API tests share state by writing to a common database table?

No. API tests should be independent. Create any data you need inside the test or in a setup fixture, and clean it up in teardown. Shared state causes ordering dependencies, flaky failures in parallel runs, and debugging nightmares when one test corrupts another's data.

9 Interview Prep — What They'll Ask

Q1. "How do you handle authentication in an API test suite?"

I use a fixture or setup method to fetch a token before the suite runs, storing it in a session-scoped variable. For token refresh, I wrap requests in a helper that catches 401s, refreshes the token, and retries once. Tokens never appear in committed code; they are injected via environment variables or CI secrets.

Q2. "What is contract testing, and how is it different from API testing?"

API testing verifies that a provider's endpoint behaves correctly. Contract testing verifies that the consumer's expectations match the provider's promises, often using tools like Pact. It decouples teams: the consumer team writes the contract, and the provider team ensures they do not break it. At mid-level, you should be comfortable with both.

Q3. "How do you test an API that depends on a third-party service?"

Use mocking or stubbing for the third-party dependency in unit and integration tests. In a small number of contract tests, hit the real sandbox endpoint to confirm the integration still works. Never rely on the third party's production environment for automated tests. WireMock and Mountebank are common tools for this.

Q4. "Describe your approach to testing a POST endpoint that creates a resource."

I test the happy path (valid payload, 201, returned ID), schema validation (required fields, data types), negative cases (missing auth, malformed JSON, duplicate creation), and idempotency if the spec claims it. I also verify the resource can be retrieved with GET after creation, proving persistence.