Pytest organizes test execution and diagnostics, while Requests sends HTTP requests and exposes responses for inspection. Together they provide a simple Python API automation stack. Their strength is explicit setup, actions, and expected business outcomes without a browser layer.
The ease of a first request can be misleading. A quality API test checks permissions, state, and operation consequences alongside a 200 code. It needs controlled data, bounded waiting, and clear information about the violated rule.
Preparing the project
Create a separate virtual environment using a supported Python version and install pytest and requests. In a work project, pin dependencies with the chosen manager and retain a lockfile or agreed requirements file. Obtain the test API address and credentials from environment configuration.
Avoid hardcoding a production address in a shared client. Validate the allowed environment before the first state-changing operation. Secrets must not appear in parameter names, reports, or assertion messages. The following examples use only local synthetic data without authorization.
A standalone local example
Save the code as test_orders.py and run python -m pytest -v test_orders.py. The fixture starts its own HTTP server on an available local port and stops it afterward. Installed pytest and requests packages are the only external Python dependencies.
import json
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import pytest
import requests
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
found = self.path == '/orders/42'
data = {'id': 42, 'status': 'draft'} if found else {'code': 'not_found'}
body = json.dumps(data).encode()
self.send_response(200 if found else 404)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
@pytest.fixture
def api_url():
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
yield f'http://127.0.0.1:{server.server_port}'
finally:
server.shutdown()
server.server_close()
thread.join()
@pytest.mark.parametrize('path, status, expected', [
('/orders/42', 200, {'id': 42, 'status': 'draft'}),
('/orders/999', 404, {'code': 'not_found'}),
])
def test_order_read(api_url, path, status, expected):
response = requests.get(api_url + path, timeout=(2, 3))
assert response.status_code == status
assert response.headers['Content-Type'].startswith('application/json')
assert response.json() == expectedExpect two independent passing cases: an existing order and a missing resource. This demonstrates clients and fixtures against a controlled server rather than testing a real order service. In a product, tests call the application, and expectations come from requirements and prepared data.
Why timeout matters
Without an explicit timeout, Requests can wait indefinitely. The example's tuple limits connection establishment and read waits. It is not a total download deadline: a response delivering data gradually has different timing semantics.
Choose values appropriate to the test environment and distinguish network failures from HTTP errors. ConnectionError concerns obtaining a response; 403 is a received denial response. Do not automatically classify both as an authorization defect or hide them under the same broad except.
Status, body, and effects
Check the agreed status, structure, significant values, and state change. After creating an order, read it back and verify owner and total. An id alone is insufficient: the server may have stored incorrect data or returned another user's record.
response.json() successfully parsing JSON does not establish operation success. For expected errors, do not automatically call raise_for_status before checking the error contract: its exception will interrupt the meaningful assertion. Positive and negative scenarios must explicitly define acceptable outcomes.
For asynchronous APIs, distinguish request acceptance from completion. A 202 response may create an operation whose state is read through a separate identifier. Polling needs a deadline and a terminal-state check, without creating a new operation on every attempt. On timeout, retain the last state and observation time. This gives a more meaningful result than a fixed pause followed by one read.
Explicitly configure redirect handling where redirects are unexpected. Otherwise, redirection to a login page may finish with 200 and mislead a simple status check. Negative scenarios should also verify no unwanted effect: a rejected operation must not create a record, send an event, or change stock. Select expectations from the contract rather than imposing arbitrary internal implementation details.
Fixtures and the data lifecycle
A fixture describes resource setup and guaranteed cleanup after yield. Separate users, API clients, and individual orders to keep responsibilities clear. Session scope suits immutable shared resources, while mutable entities usually need a narrower lifetime.
Cleanup should track created IDs and avoid deleting everything matching a shared characteristic. Otherwise, parallel tests may delete one another's data. If creation fails before yield, consider which completed operations still need cleanup.
Requests Session and configuration
Session reuses connections and common settings. Its cookies and headers are state, however. Different roles need separate instances or explicit management; otherwise, a buyer's request may accidentally use administrator credentials.
A shared client should reduce duplication in base URLs, timeouts, and sensitive-log sanitization. It should not automatically retry every error. Repeating a state-changing operation requires understood idempotency and should itself be part of the tested behavior.
A practical access check
Prepare two users and an order owned by the first. The first reads it and receives correct data. The second requests the same ID and receives the contract-defined rejection without the other user's order body. Check an anonymous request separately.
API requirements determine statuses: 403 and 404 may serve different policies. QA checks policy consistency and absence of leakage. JSON schema checks alone cannot detect an ownership violation; concrete values and roles are necessary.
Mistakes and diagnostics
Common mistakes include checking only status_code, disabling TLS validation for every error, using real data, and sharing one Session across a parallel suite. Random data without a seed or retained inputs also makes failures irreproducible.
Retain the method, sanitized URL, status, relevant fields, and correlation ID. Pytest and Requests become professional tools when each case has a clear purpose, independent setup, and a verifiable operation outcome.
Agree on contract expectations with developers before implementation where possible. This prevents automating an arbitrary current response as mandatory product behavior.