API Testing
PyTest API Testing Tutorial with Examples
Learn how to test REST APIs with Python, PyTest, and requests using practical examples for QA engineers and automation beginners.
PyTest and Python's requests library give QA engineers a practical way to turn stable API expectations into automated regression tests. The requests library sends HTTP requests. PyTest discovers tests, supplies fixtures, evaluates assertions, and reports failures.
This beginner-friendly tutorial uses an illustrative Job Tracker API. The endpoints and payloads are practice examples, not live production results. Use a safe test environment and adapt every expected status, field, and error to your API's documented contract.
If requests and responses are new to you, start with what API testing is. If you have not built requests manually, work through the Postman REST API testing tutorial first. Code is easier to trust after you understand the behavior you are automating.
What You Need Before Writing PyTest API Tests
You do not need a large framework to write your first API test, but you do need a few foundations:
- basic Python, including functions, dictionaries, lists, imports, and variables;
- a current Python installation;
- PyTest and
requestsinstalled in an isolated virtual environment; - the base URL for a safe test API;
- documentation or clear expected behavior for the endpoint;
- controlled test data that your test can create, use, and clean up;
- test credentials supplied securely if the API requires authentication.
Never place a real token, password, private key, or production credential in a test file. Load secrets from an approved secret store or protected environment variable. Do not casually run write, update, or delete tests against production. A test that creates records can affect customers, analytics, integrations, and data quality even when the request itself looks harmless.
Install PyTest and requests
Create a virtual environment so the tutorial dependencies stay isolated from other Python projects:
python -m venv .venv
source .venv/bin/activate
python -m pip install pytest requests
Windows virtual-environment activation uses a different command depending on the shell. The Python installation guide for your environment will show the appropriate command.
Confirm that PyTest is available:
python -m pytest --version
Using python -m pytest makes it clear which Python environment runs the tests.
Configure the API Base URL With a Fixture
A base URL identifies the test environment, such as a local service or dedicated QA deployment. Do not repeat it inside every test. Put this fixture in conftest.py so PyTest can make it available to test functions:
import os
import pytest
@pytest.fixture(scope="session")
def base_url():
value = os.getenv("API_BASE_URL")
if not value:
pytest.fail("Set API_BASE_URL to a safe test environment")
return value.rstrip("/")
Set the variable before running the suite. This hostname is intentionally reserved for documentation and does not claim to host a working API:
export API_BASE_URL="https://api.example.test"
python -m pytest
Environment-based configuration lets the same tests target an approved local, QA, or staging environment without editing the test code. A larger project may load configuration from command-line options or a settings module. Keep the same principle: environment details belong outside individual test scenarios.
Your First PyTest API Test: GET /api/jobs
Assume the practice Job Tracker exposes this endpoint:
GET /api/jobs
Create tests/test_jobs_api.py:
import requests
def test_jobs_list_returns_200(base_url):
response = requests.get(f"{base_url}/api/jobs", timeout=10)
assert response.status_code == 200
Run the test:
python -m pytest tests/test_jobs_api.py -v
PyTest finds functions whose names start with test_. It sees the base_url parameter, finds the fixture with that name, and passes the configured value into the test. The request includes a timeout so a network problem cannot make the test wait forever.
This first check answers only one question: did the endpoint return 200? A useful API test usually validates more.
Validate the JSON Response Fields
Suppose the controlled test user has at least one known job and a successful response is a JSON list. Add a separate test for the response structure:
import requests
def test_jobs_list_has_expected_fields(base_url):
response = requests.get(f"{base_url}/api/jobs", timeout=10)
assert response.status_code == 200
assert response.headers["Content-Type"].startswith("application/json")
jobs = response.json()
assert isinstance(jobs, list)
assert jobs, "Expected at least one job in controlled test data"
first_job = jobs[0]
required_fields = {"id", "company", "title", "status"}
assert required_fields.issubset(first_job)
assert isinstance(first_job["id"], int)
assert isinstance(first_job["company"], str)
assert isinstance(first_job["title"], str)
assert isinstance(first_job["status"], str)
The non-empty assertion is correct only because the test setup promises a job exists. If an empty list is valid for the user, test that behavior separately and create explicit data for the populated-list scenario. Assertions must match their preconditions.
For a larger suite, consider validating every item or using a documented JSON schema. Do not assert every response value by default. Focus on the fields, types, and business rules that consumers rely on.
Test Create Behavior With POST /api/jobs
Now assume the practice API creates a record here:
POST /api/jobs
The illustrative payload is:
{
"company": "Acme QA",
"title": "QA Engineer",
"status": "applied"
}
A PyTest example can verify the create response and remove the test record afterward:
import requests
def test_create_job_returns_created_record(base_url):
payload = {
"company": "Acme QA",
"title": "QA Engineer",
"status": "applied",
}
response = requests.post(
f"{base_url}/api/jobs",
json=payload,
timeout=10,
)
assert response.status_code == 201
created_job = response.json()
assert created_job["id"]
job_id = created_job["id"]
try:
assert created_job["company"] == payload["company"]
assert created_job["title"] == payload["title"]
assert created_job["status"] == payload["status"]
get_response = requests.get(
f"{base_url}/api/jobs/{job_id}",
timeout=10,
)
assert get_response.status_code == 200
assert get_response.json()["id"] == job_id
finally:
delete_response = requests.delete(
f"{base_url}/api/jobs/{job_id}",
timeout=10,
)
assert delete_response.status_code in (200, 204)
This example assumes 201 Created, a returned id, a GET /api/jobs/{id} endpoint, and a delete endpoint that returns 200 or 204. Your API may define different behavior. Follow its contract.
The finally block attempts cleanup even if a later assertion fails. A real framework often moves creation and cleanup into fixtures or API client helpers. It may also attach authorization headers to every request. Keep write tests in a controlled environment where cleanup is allowed and where one failed cleanup cannot damage shared data.
Write Negative API Tests
Positive tests prove valid requests work. Negative tests verify that the API rejects invalid input and unauthorized behavior safely and consistently.
Missing a required field
If title is required, send a payload without it and validate the documented error:
import requests
def test_create_job_rejects_missing_title(base_url):
payload = {
"company": "Acme QA",
"status": "applied",
}
response = requests.post(
f"{base_url}/api/jobs",
json=payload,
timeout=10,
)
assert response.status_code == 400
error = response.json()
assert error["field"] == "title"
assert error["message"]
Invalid status values
PyTest parametrization can run the same rule against representative invalid values:
import pytest
import requests
@pytest.mark.parametrize("invalid_status", ["", "unknown", 42, None])
def test_create_job_rejects_invalid_status(base_url, invalid_status):
payload = {
"company": "Acme QA",
"title": "QA Engineer",
"status": invalid_status,
}
response = requests.post(
f"{base_url}/api/jobs",
json=payload,
timeout=10,
)
assert response.status_code == 400
The supported values and error status belong to the API contract. Do not assume every validation failure returns 400 or every API uses the same error shape.
Authorization and unknown resources
Useful negative scenarios also include:
- a request without required authentication returns the documented unauthorized response;
- a valid user cannot retrieve or modify another user's job;
GET /api/jobs/999999returns the documented not-found response;- a duplicate submission follows the defined uniqueness or idempotency rule;
- malformed JSON returns a safe client error rather than a server error;
- error responses do not reveal stack traces, tokens, database details, or other sensitive information.
Use separate test users and known records for permission tests. Never place a real bearer token directly in the source file.
Good API Test Assertions Checklist
A green status code does not prove the complete behavior is correct. Select assertions that answer the important questions for the scenario:
- Is the status code correct for this exact outcome?
- Is the content type correct before parsing JSON?
- Does the response body have the expected shape?
- Are required fields present, and do values have the right types?
- Do returned business values match the request and known test data?
- Does an error response identify the problem in the documented format?
- Are authentication, ownership, and role permissions enforced?
- Does the response avoid exposing secrets or unrelated private data?
- Did a create, update, or delete request produce the expected side effect?
- Did the test leave the environment in a safe state?
Avoid asserting irrelevant implementation details. A test becomes brittle when it fails on harmless changes that do not affect the product contract.
A Simple PyTest API Project Structure
Start small and separate reusable request logic only when repetition appears:
api-tests/
├── conftest.py
├── clients/
│ └── jobs_client.py
└── tests/
└── test_jobs_api.py
conftest.pycontains shared fixtures such as the base URL, authentication, and controlled test data.clients/jobs_client.pycan wrap repeated requests such ascreate_job()anddelete_job().tests/test_jobs_api.pydescribes product behavior with focused tests and assertions.
As the suite grows, you may add configuration, schemas, fixture modules, reports, and tests grouped by business domain. The API testing automation guide explains how configuration, auth helpers, API clients, fixtures, tests, assertions, reports, and CI/CD fit into a maintainable framework.
Common PyTest API Testing Mistakes
Using production as the test environment
Write tests can create or damage real data and trigger real integrations. Use a safe, approved environment with controlled accounts and cleanup.
Depending on uncontrolled shared data
A test that expects “the first job” to exist may fail because another person changed the environment. Create explicit data, isolate test users, or make the precondition clear.
Checking only the status code
An endpoint can return 200 with the wrong fields, another user's data, or an HTML error page. Validate the response and business behavior that matter.
Omitting request timeouts
Every network request should have an appropriate timeout. Without one, an unavailable service can stall the suite and waste CI/CD time.
Hardcoding secrets
Do not commit tokens, passwords, keys, or private environment details. Load secrets through the team's approved secure mechanism and prevent them from appearing in logs.
Making tests depend on execution order
Each test should prepare the state it needs or use an explicit fixture. A test that passes only after another test has run is hard to debug and cannot run reliably by itself.
Skipping cleanup
Created records can accumulate, collide with later runs, and make results unpredictable. Use fixtures or try and finally when cleanup is required, and make cleanup failures visible.
Building a framework before learning the API
Start with one readable test. Add clients, fixtures, and abstractions when real repetition or maintenance needs appear. A large framework cannot compensate for unclear expected behavior.
A Practical Learning Sequence
- Learn testing fundamentals and how to select useful positive and negative cases.
- Understand methods, endpoints, headers, JSON, status codes, and authentication.
- Practice requests manually in Postman until you can explain each input and response.
- Decide what API behavior should be automated.
- Implement one stable
GETtest with PyTest andrequests. - Add controlled test data, fixtures, create behavior, negative cases, and cleanup.
- Run trusted tests in CI/CD and expand coverage based on product risk.
The QA Automation Engineer Roadmap places Python and API testing alongside databases, UI automation, Docker, and delivery pipelines. Keep the tutorials and cheat sheets nearby as practical references.
Frequently Asked Questions
Is PyTest good for API testing?
Yes. PyTest provides readable tests, fixtures, parametrization, assertions, plugins, and CI/CD-friendly reporting. Pair it with an HTTP library such as requests to send API calls. Its value still depends on clear test design, controlled data, and maintainable setup.
How do I test an API using PyTest?
Install PyTest and an HTTP client, configure a safe base URL, write a test function, send a request, and assert the documented response. Start with a simple GET, then add JSON checks, negative cases, fixtures, create behavior, and cleanup as needed.
What library do I use with PyTest for API testing?
The Python requests library is a common beginner-friendly choice for synchronous HTTP requests. Other projects may use httpx, an application-specific test client, or a generated SDK. PyTest organizes and runs the tests regardless of the HTTP client.
Should I learn Postman before PyTest API testing?
For most beginners, yes. Postman makes methods, URLs, headers, bodies, and responses visible before code adds another layer. Once you understand a stable request and its expected result, automate the checks worth repeating.
Can PyTest API tests run in CI/CD?
Yes. A CI/CD runner can install the dependencies, supply approved environment configuration and secrets, run python -m pytest, and publish the results. Keep the suite deterministic and make failures specific enough for the team to act on.
What should a beginner test first?
Start with one safe GET request. Check the status code, content type, JSON shape, required fields, and key data types. Then add one clear negative case. Move to create and cleanup scenarios after you can control the environment and test data safely.
Keep Building Your API Automation Skills
The most useful PyTest API tests begin with a well-understood product rule. Practice the request manually, make the test data explicit, write focused assertions, and keep the failure message connected to the behavior under test.
If you are still building your QA foundation, start with the free Software Testing Fundamentals course. Then continue through the API concept, Postman practice, automation strategy, and PyTest implementation pages in this cluster.
