## From manual to automated API testing
If you've tested APIs with a REST client, you know the loop: send a request, eyeball the response, move on. Automated API tests do exactly the same thing — but repeatably, in milliseconds, and they alert you the moment something regresses.
With pytest and the `requests` library, an API test looks nearly identical to a manual check:
```python
import requests
def test_products_list():
resp = requests.get('https://apilearn.tukas.dev/api/products/')
assert resp.status_code == 200
```
The difference: this runs automatically every time you push, with no one looking at it.
## What to assert in an API test
Assert in layers — each layer builds on the previous:
1. **Status code** — is the server responding at all, and with the right code?
2. **JSON structure** — does the response have the keys you expect?
3. **Values** — are specific values correct for this input?
```python
def test_products_list():
resp = requests.get('https://apilearn.tukas.dev/api/products/')
assert resp.status_code == 200 # layer 1: status
data = resp.json()
assert 'results' in data # layer 2: structure
assert 'count' in data
assert isinstance(data['results'], list) # layer 2: type
assert len(data['results']) > 0 # layer 3: value
```
Start with the status code. If it's wrong, the rest is noise — your response body might be an error page, not JSON at all.
## BASE_URL: one place to change
Define the API base URL once and reference it everywhere. This makes switching between environments (staging, local, production) a one-line change:
```python
BASE_URL = 'https://apilearn.tukas.dev'
def test_products():
resp = requests.get(f'{BASE_URL}/api/products/')
assert resp.status_code == 200
```
Better yet, put it in `conftest.py` as a fixture — then all test files share it and you can override it per directory.
## Authentication: extract into a fixture
Repeating the login flow in every test is fragile. Extract it to a fixture:
```python
# conftest.py
import pytest
import requests
BASE_URL = 'https://apilearn.tukas.dev'
@pytest.fixture(scope='session')
def auth_token(base_url):
resp = requests.post(f'{base_url}/api/auth/token/', json={
'username': 'testuser',
'password': 'TestUser2024!',
})
resp.raise_for_status()
return resp.json()['token']
@pytest.fixture
def auth_headers(auth_token):
return {'Authorization': f'Token {auth_token}'}
```
Any test that needs authentication accepts `auth_headers` as a parameter — pytest injects it:
```python
def test_profile(base_url, auth_headers):
resp = requests.get(f'{base_url}/api/users/profile/', headers=auth_headers)
assert resp.status_code == 200
```
Making `auth_token` session-scoped means the login call happens once per test run, not once per test.
## Designing isolated, repeatable tests
A good API test:
- **Doesn't depend on test execution order** — it either reads public/stable data, or sets up its own state
- **Leaves no side effects** that break other tests — use yield fixtures to clean up anything you create
- **Asserts concretely** — check specific values, not just that the response is non-empty
For read-only endpoints (GET), tests are naturally isolated. For write operations, clean up with a yield fixture:
```python
@pytest.fixture
def created_cart_item(base_url, auth_headers):
resp = requests.post(
f'{base_url}/api/cart/items/',
json={'product_id': 1, 'quantity': 1},
headers=auth_headers,
)
item_id = resp.json()['id']
yield item_id
requests.delete(f'{base_url}/api/cart/items/{item_id}/', headers=auth_headers)
```
## requests.Response — what you get back
```python
resp = requests.get(url, headers={}, params={})
resp.status_code # int: 200, 201, 404, 500, etc.
resp.json() # dict or list — parsed JSON body
resp.text # str — raw response body
resp.headers # dict-like — response headers (case-insensitive keys)
resp.url # str — final URL after any redirects
resp.history # list — intermediate responses in redirect chain
resp.raise_for_status() # raises requests.HTTPError if status_code >= 400
```
Write a test that calls `GET https://apilearn.tukas.dev/api/products/` and asserts: (1) status code is 200, (2) the JSON response contains a `"results"` key, (3) `"results"` is a list, (4) the list is not empty. Run `pytest -v` and confirm the test passes.
import requests
BASE_URL = 'https://apilearn.tukas.dev'
def test_products_list():
resp = requests.get(f'{BASE_URL}/api/products/')
# assert status code 200
# assert 'results' in response JSON
# assert results is a list
# assert results is not empty
pass
Solution
import requests
BASE_URL = 'https://apilearn.tukas.dev'
def test_products_list():
resp = requests.get(f'{BASE_URL}/api/products/')
assert resp.status_code == 200
data = resp.json()
assert 'results' in data
assert isinstance(data['results'], list)
assert len(data['results']) > 0
Write a test that calls GET https://apilearn.tukas.dev/api/products/ with query params page=1 and page_size=3. Assert that the response status is 200 and that len(data["results"]) == 3. Use the `params` keyword argument of `requests.get` to pass query parameters — do not build the query string manually.
Write a conftest.py with: (1) a session-scoped `auth_token` fixture that POSTs to https://apilearn.tukas.dev/api/auth/token/ with username="testuser" and password="TestUser2024!" and returns the token string; (2) a function-scoped `auth_headers` fixture that returns {"Authorization": "Token <token>"}. Then write a test that uses `auth_headers` to call GET https://apilearn.tukas.dev/api/users/profile/ and asserts status 200 and that "username" is in the response.
# conftest.py
import pytest
import requests
BASE_URL = 'https://apilearn.tukas.dev'
@pytest.fixture(scope='session')
def auth_token():
# POST to BASE_URL/api/auth/token/ with credentials dict
# return resp.json()['token']
pass
@pytest.fixture
def auth_headers(auth_token):
# return {'Authorization': f'Token {auth_token}'}
pass
# test_profile.py
def test_profile_authenticated(auth_headers):
# GET BASE_URL/api/users/profile/ with auth_headers
# assert status 200
# assert 'username' in response JSON
pass
Write a test that sends GET https://apilearn.tukas.dev/api/echo/ with a custom header X-Test-Id: pytest-exercise-4. Assert that the response status is 200 and that the JSON body reflects your header back — the echo endpoint returns request headers under a "headers" key in the response JSON.
Write two tests for GET https://apilearn.tukas.dev/api/redirect/. First: let requests follow the redirect (default) — assert status 200, that resp.history is non-empty, and that the final resp.url contains "apilearn.tukas.dev". Second: pass allow_redirects=False — assert the status is a 3xx code (301, 302, 307, or 308) and that a "Location" header is present in the response.
import requests
BASE_URL = 'https://apilearn.tukas.dev'
def test_redirect_followed():
resp = requests.get(f'{BASE_URL}/api/redirect/')
# assert status 200
# assert resp.history is not empty
# assert 'apilearn.tukas.dev' in resp.url
pass
def test_redirect_not_followed():
resp = requests.get(f'{BASE_URL}/api/redirect/', allow_redirects=False)
# assert status is 3xx
# assert 'Location' in resp.headers
pass
We use necessary cookies to run the site. With your permission, we can also save your site preferences and use analytics and advertising cookies to understand usage and support the project.
Open tools in tabs.Exercises, IDE tools, and trainers stay available as site tabs.
Switch without losing context.Move between explanations, code, and utilities while keeping your place.
Use the sidebar as your map.The left panels hold navigation, settings, files, libraries, and tool controls.
PythonJavaScriptSQLite
One IDE, three practical modes
Python in the browser.Run small scripts, try libraries, and practice API requests without installing anything.
JavaScript for quick experiments.Test browser-friendly code and compare ideas next to your learning materials.
SQLite for data practice.Open the database explorer to inspect tables, write queries, and learn SQL workflows locally.
TopicIDE
Work side by side with split tabs
Keep instructions visible.Open an exercise or reference page beside the IDE instead of jumping back and forth.
Compare tools while you learn.Place regex checks, explanations, and code experiments next to each other when the task needs it.
Close the split when you are done.The workspace returns to a single focused tab, and your open site tabs remain available.
Code Typing Trainer
Or plain text
This trainer is designed for a physical keyboard.Open this section on a laptop or desktop with a wide screen. Touch typing practice will not work correctly on a phone.
Speed: 0 chars/min
0 words/min
Best speed (60s): 0 chars/min
0 words/min
Errors: 0
Total time: 0.0 s
To practice touch typing, avoid looking at your physical keyboard.