Skip to content
CodeKruPro

API testing · Python

API automation

Sending requests and checking responses in code (REST Assured or requests).

Learn: API automation in Python with requests and pytest9 minute read

API automation means sending requests from code and checking the responses automatically, so the checks run on every build. Python teams usually do it with two libraries: requests sends the HTTP calls, and pytest finds, runs and reports the tests. Interviewers ask you to write a test for an endpoint, explain how you handle tokens and environments, and show how a failure is debugged. This article builds a small, realistic suite (a group of tests) step by step. It assumes you know basic Python and have seen JSON before (see the HTTP topic).

Sending requests and reading responses

import requests

r = requests.get("https://api.example.com/products",
                 params={"q": "red shoes", "page": 2}, timeout=10)
r.status_code        # 200
r.headers["Content-Type"]
r.json()             # the parsed body: a dict for a JSON object
r.text               # the raw body as text
r.elapsed            # time until the response headers arrived (a timedelta)

Points interviewers look for:

  • params= builds and encodes the query string; never glue it together by hand.
  • json= sends a JSON body and sets Content-Type: application/json. data= with a dict sends a form-encoded body instead (the format HTML forms use, name=Asha&email=...), a common cause of 400 or 415 errors.
  • requests has no default timeout (a limit on how long to wait). Without timeout=, a stuck server can hang a CI job (an automatic test run on a build server) for hours; with it, you get a clear requests.exceptions.Timeout.
  • requests does not raise an exception for 4xx or 5xx responses. Assert the status you expect; raise_for_status() exists but gives less helpful failures in tests.

Shared setup with fixtures

A pytest fixture is a function that prepares something tests need. Put shared fixtures in conftest.py, and pytest passes them to any test that names them as a parameter. A session-scoped fixture runs only once per test run, not once per test. Here it logs in and returns a requests.Session that sends the token on every call and keeps cookies:

# tests/conftest.py
import os
import pytest
import requests


@pytest.fixture(scope="session")
def base_url():
    return os.environ.get("BASE_URL", "https://staging.example.com")


@pytest.fixture(scope="session")
def api(base_url):
    session = requests.Session()
    r = session.post(f"{base_url}/auth/login",
                     json={"username": os.environ.get("API_USER", "admin"),
                           "password": os.environ.get("API_PASSWORD", "secret")},
                     timeout=10)
    assert r.status_code == 200, r.text
    session.headers.update({"Authorization": f"Bearer {r.json()['token']}"})
    yield session
    session.close()

The yield line hands the session to the tests. The code after it is teardown (cleanup) and runs when the tests are done, even when tests fail. Credentials come from environment variables, filled from the CI secret store in pipelines; the default values here only suit a local test server.

A test for a POST endpoint

# tests/test_users.py
import uuid
import pytest
import requests


def test_create_user(api, base_url):
    email = f"asha-{uuid.uuid4().hex[:8]}@example.com"
    r = api.post(f"{base_url}/users", json={"name": "Asha", "email": email}, timeout=10)
    assert r.status_code == 201, f"{r.request.method} {r.url} -> {r.status_code}: {r.text}"
    created = r.json()
    assert created["email"] == email

    saved = api.get(f"{base_url}/users/{created['id']}", timeout=10)
    assert saved.status_code == 200
    assert saved.json()["name"] == "Asha"


@pytest.mark.parametrize("email", ["", "plainaddress", "asha.example.com"])
def test_rejects_invalid_email(api, base_url, email):
    r = api.post(f"{base_url}/users", json={"name": "Asha", "email": email}, timeout=10)
    assert r.status_code == 400
    assert r.json()["field"] == "email"


def test_needs_token(base_url):
    r = requests.get(f"{base_url}/users/1", timeout=10)
    assert r.status_code == 401

Why this suite is good:

  • A unique email per run, so the test can run again and in parallel.
  • The assertion message carries the method, URL, status and body, so a CI failure explains itself.
  • The GET afterwards proves the user was saved, not only that the API said so.
  • parametrize runs the negative test once per value, and each value is reported separately.
  • The token test deliberately skips the shared session to prove the API rejects anonymous calls (calls with no token).

The line @pytest.mark.parametrize is a decorator, a line placed above a function that changes how it runs. Here it runs the function once for each email in the list.

Checking the response shape

The jsonschema package validates a whole body against a JSON Schema, a description of which fields a body must have and of which types:

from jsonschema import validate

USER_SCHEMA = {"type": "object", "required": ["id", "name", "email"],
               "properties": {"id": {"type": "integer"}, "name": {"type": "string"},
                              "email": {"type": "string"}}}

validate(instance=r.json(), schema=USER_SCHEMA)   # raises ValidationError if wrong

Use it for shape (fields, types, allowed values) and plain asserts for the values the test is about.

Other everyday tasks

  • Files: files={"file": open("invoice.pdf", "rb")} sends multipart/form-data; use a with block so the file is closed.
  • Basic auth: auth=("user", "password") builds the Basic header.
  • Response time: assert r.elapsed.total_seconds() < 2 catches big slowdowns, but it is not a load test.
  • Logging: a response hook on the session, or a small helper, can log every call through the logging module, and pytest shows captured logs for failing tests. Hide (mask) tokens first.
  • Typed bodies: small dataclass models (plain classes that only hold fields) with asdict() keep request bodies tidy. This is the Python answer to "do you use POJOs?", a Java term for plain classes that hold data.

Running it

pip install -r requirements.txt          # requests, pytest, jsonschema, pinned
BASE_URL=https://staging.example.com pytest -m "not slow" --junitxml=reports/api.xml

The first command installs the libraries listed in requirements.txt. In the second, -m "not slow" skips tests marked with @pytest.mark.slow, and --junitxml writes the results as JUnit XML, a common test report format. That file gives the CI server a per-test report, and pytest's exit code (a number returned at the end; 0 only when everything passed) decides whether the pipeline step passes.

How to answer in an interview

Write the POST test above from memory and explain each line: unique data, json=, timeout=, the status with a helpful message, the body checks, the read-back. Then describe the suite: fixtures in conftest.py for the base URL and a logged-in session, parametrize for validation rules, settings and secrets from environment variables, and a JUnit report in CI.

Common mistakes

  • Leaving out timeout=, so one stuck call hangs the whole job.
  • Sending JSON with data= instead of json=.
  • Bare asserts with no message, so CI shows only assert 400 == 201.
  • Hard-coded passwords and fixed test data that clash on the second run.

Recap

requests sends the calls and pytest runs them. Pass json= and timeout= on every call, assert the exact status with a useful message, read the data back, and keep shared setup in fixtures. A schema checks shape, asserts check values, and the JUnit report plus exit code tell CI the result.

13 questions0/13 solved

StatusQuestionDifficultyAsked in
Not startedHow do you automate API tests in Python?EasyAsked in: Capgemini, Coforge, Wipro
Not startedrequests: json= or data= for a JSON body?EasyAsked in: GreyOrange, TCS
Pro+Reading a requests responseEasy-MediumAsked in: Capgemini, CloudSEK, Swiggy +1
Pro+Why pass timeout= to every requests call?Easy-Medium
Pro+Reuse a token and cookies with requests.SessionMediumAsked in: Capgemini
Pro+Write an automated test for a POST endpoint in PythonMediumAsked in: GreyOrange, TCS
Pro+Query parameters with requestsEasy-MediumAsked in: Capgemini, Wipro
Pro+Validate a response schema in PythonMedium
Pro+Fixtures and parametrize in an API suiteMedium
Pro+Log requests and responses when a Python API test failsMedium
Pro+Check response time with requestsMedium
Pro+Upload a file through an API with requestsMedium
Pro+Typed models for request and response bodies in PythonMedium-HardAsked in: Accenture, Capgemini
Not startedNot startedIn progressIn progressSolvedSolvedPro+Pro+

The first 2 questions of each topic are free. Pro+ opens every question.

Back to the plan