By now you have everything: variables and strings, dicts and lists, loops,
functions, assert and the pytest shape. What is left is assembling the thing it
was all for — API autotests.
How a test project is laid out
tests/
conftest.py shared fixtures: client, login, test data
test_products.py catalogue tests
test_orders.py order tests
requirements.txt libraries
Exactly two libraries are needed: pytest runs the tests, requests sends the
requests.
pip install pytest requests
conftest.py creates the client — an object that remembers the address and the
headers:
import pytest
import requests
BASE = "https://shop.example.com/api"
@pytest.fixture
def client():
return requests.Session()
@pytest.fixture
def headers(client):
response = client.post(f"{BASE}/auth/login",
json={"email": "anna@example.com", "password": "secret123"})
assert response.status_code == 200, "login failed — everything after this is meaningless"
return {"Authorization": "Bearer " + response.json()["token"]}
In our trainer the
client fixture is ready and talks to a toy service — the test code is the same,
with nothing to configure.
What to check in a response
Four layers, from cheap to valuable.
The status code. First and always: 200, 201, 404, 422.
The fields that matter — not the whole body: comparing against a complete dict breaks on any new field, and adding a field does not break compatibility.
Headers that are part of the contract — for example Location on a created
object.
Consequences in the system. The most valuable and most often forgotten: the order was created — did the stock go down? A 201 does not say that.
before = client.get(f"{BASE}/products/p-01").json()["inStock"]
client.post(f"{BASE}/orders", json={"productId": "p-01", "quantity": 2}, headers=headers)
assert client.get(f"{BASE}/products/p-01").json()["inStock"] == before - 2
Rejections matter as much as happy paths
Tests that only walk the happy path miss half the defects. For every scenario ask three questions: what happens with invalid data, without permissions, on a conflict.
live example
def test_order_without_token(client):
response = client.post(f"{BASE}/orders", json={"productId": "p-01", "quantity": 1})
assert response.status_code == 401
def test_empty_body(client, headers):
response = client.post(f"{BASE}/orders", json={}, headers=headers)
assert response.status_code == 422
fields = [d["field"] for d in response.json()["error"]["details"]]
assert "productId" in fields
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Free week →
And separately — that the rejection changed nothing: the stock is intact, no order was created. A rejection that managed to change data is a classic finding in permission testing.
Data is prepared by request, not by clicking
A test that needs a paid order should not go through the interface. It creates the state with requests — fast and repeatable:
@pytest.fixture
def paid_order(client, headers):
return client.post(f"{BASE}/orders",
json={"productId": "p-01", "quantity": 1}, headers=headers).json()
And every test prepares its own data. Tests that hand state to each other break when the run order changes and fail "in turns" for no reason.
A green test proves nothing
The main point of the whole section. A test is code too, and code has bugs. A test that checks nothing is always green:
live example
def test_order(client, headers):
client.post(f"{BASE}/orders", json={"productId": "p-01", "quantity": 1}, headers=headers)
assert True # this happens more often than you would like
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Free week →
There is one honest way to check your tests: break the system and see whether they notice. Turn off validation, drop the token check, stop decreasing the stock — how many tests go red? The ones still green check nothing.
The technique is called mutation testing, and in the API autotest trainer it runs automatically: your tests are executed against nine broken versions of the service, and the report shows which breakages they caught.
Where to go next
When API tests come easily, the usual next steps are: running them in the build pipeline, reports and flaky-test triage, and only then interface tests — they are more expensive and more capricious.
In short
- A test project needs two libraries:
pytestandrequests; shared fixtures live inconftest.py. - Four layers get checked: status code, meaningful fields, contract headers, and consequences in the system.
- Rejections are checked alongside happy paths — and separately, that the rejection changed nothing.
- Data is prepared with requests, by each test for itself.
- A green test proves nothing until it has shown that it goes red on a broken system.
What to read next
- pytest: your first real test — if you need the shape again.
- API testing in Postman — the same requests by hand, for a quick look.