Python · Testing with pytest · Intermediate

Testing HTTP APIs

5 tasks

Write automated tests for HTTP APIs using requests and pytest fixtures.

Testing HTTP APIs with pytest

#
## 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 ```

API Tests Against apilearn.tukas.dev

#
## Shared setup in conftest.py ```python # conftest.py import pytest import requests BASE_URL = 'https://apilearn.tukas.dev' @pytest.fixture(scope='session') def base_url(): return BASE_URL @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}'} ``` ## test_products.py — status, structure, values ```python # test_products.py 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 'count' in data assert isinstance(data['results'], list) def test_products_pagination(): resp = requests.get( f'{BASE_URL}/api/products/', params={'page': 1, 'page_size': 3}, ) assert resp.status_code == 200 assert len(resp.json()['results']) == 3 ``` ## test_auth.py — login and protected endpoints ```python # test_auth.py import requests BASE_URL = 'https://apilearn.tukas.dev' def test_token_login(): resp = requests.post(f'{BASE_URL}/api/auth/token/', json={ 'username': 'testuser', 'password': 'TestUser2024!', }) assert resp.status_code == 200 data = resp.json() assert 'token' in data assert isinstance(data['token'], str) assert len(data['token']) > 10 def test_profile_requires_auth(): resp = requests.get(f'{BASE_URL}/api/users/profile/') assert resp.status_code == 401 def test_profile_with_token(auth_headers): resp = requests.get( f'{BASE_URL}/api/users/profile/', headers=auth_headers, ) assert resp.status_code == 200 data = resp.json() assert 'username' in data assert 'email' in data ``` ## test_misc.py — echo and redirect ```python # test_misc.py import requests BASE_URL = 'https://apilearn.tukas.dev' def test_echo_header(): custom_headers = {'X-Custom-Header': 'pytest-test-123'} resp = requests.get(f'{BASE_URL}/api/echo/', headers=custom_headers) assert resp.status_code == 200 echoed = resp.json()['headers'] assert echoed.get('X-Custom-Header') == 'pytest-test-123' def test_redirect_followed(): # requests follows redirects automatically by default resp = requests.get(f'{BASE_URL}/api/redirect/') assert resp.status_code == 200 assert len(resp.history) > 0 # at least one redirect occurred assert 'apilearn.tukas.dev' in resp.url def test_redirect_not_followed(): resp = requests.get(f'{BASE_URL}/api/redirect/', allow_redirects=False) assert resp.status_code in (301, 302, 307, 308) assert 'Location' in resp.headers ``` ## Asserting response content precisely ```python def test_product_fields(base_url): resp = requests.get(f'{base_url}/api/products/') products = resp.json()['results'] # Assert structure of the first item first = products[0] assert 'id' in first assert 'name' in first assert 'price' in first assert isinstance(first['price'], (int, float)) assert first['price'] > 0 ```

API Testing Quick Reference

#
**Common assertion pattern:** ```python resp = requests.get(url, headers=headers, params=params) assert resp.status_code == 200 data = resp.json() assert 'key' in data assert data['key'] == expected_value ``` **requests.Response properties:** | Property | Type | Description | |----------|------|-------------| | `.status_code` | int | HTTP status (200, 201, 401, 404…) | | `.json()` | dict/list | Parsed JSON body | | `.text` | str | Raw response body | | `.headers` | dict-like | Response headers | | `.url` | str | Final URL after redirects | | `.history` | list | Redirect chain | | `.raise_for_status()` | — | Raises `HTTPError` if status ≥ 400 | **HTTP methods:** ```python requests.get(url, params={}, headers={}) requests.post(url, json={}, headers={}) # JSON body requests.post(url, data={}, headers={}) # form-encoded body requests.put(url, json={}, headers={}) requests.delete(url, headers={}) ``` **Auth header fixture (conftest.py):** ```python @pytest.fixture(scope='session') def auth_token(base_url): resp = requests.post(f'{base_url}/api/auth/token/', json={...}) return resp.json()['token'] @pytest.fixture def auth_headers(auth_token): return {'Authorization': f'Token {auth_token}'} ``` **Redirect control:** ```python requests.get(url) # follows redirects (default) requests.get(url, allow_redirects=False) # stops at first redirect ```
01

Test GET /api/products/ — Status and Structure

#

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
02

Test Pagination — Exact Item Count

#

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.

import requests

BASE_URL = 'https://apilearn.tukas.dev'


def test_products_pagination():
    resp = requests.get(
        f'{BASE_URL}/api/products/',
        # pass params as a dict: {'page': 1, 'page_size': 3}
    )

    # assert status 200
    # assert exactly 3 results
    pass
Solution
import requests

BASE_URL = 'https://apilearn.tukas.dev'


def test_products_pagination():
    resp = requests.get(
        f'{BASE_URL}/api/products/',
        params={'page': 1, 'page_size': 3},
    )

    assert resp.status_code == 200
    data = resp.json()
    assert len(data['results']) == 3
03

Auth Header Fixture and Protected Endpoint

#

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
Solution
# conftest.py
import pytest
import requests

BASE_URL = 'https://apilearn.tukas.dev'

@pytest.fixture(scope='session')
def auth_token():
    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}'}


# test_profile.py
import requests

BASE_URL = 'https://apilearn.tukas.dev'

def test_profile_authenticated(auth_headers):
    resp = requests.get(f'{BASE_URL}/api/users/profile/', headers=auth_headers)
    assert resp.status_code == 200
    data = resp.json()
    assert 'username' in data
04

Send a Custom Header and Assert It Is Echoed

#

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.

import requests

BASE_URL = 'https://apilearn.tukas.dev'


def test_echo_custom_header():
    custom_headers = {'X-Test-Id': 'pytest-exercise-4'}
    resp = requests.get(f'{BASE_URL}/api/echo/', headers=custom_headers)

    # assert status 200
    # assert resp.json()['headers']['X-Test-Id'] == 'pytest-exercise-4'
    pass
Solution
import requests

BASE_URL = 'https://apilearn.tukas.dev'


def test_echo_custom_header():
    custom_headers = {'X-Test-Id': 'pytest-exercise-4'}
    resp = requests.get(f'{BASE_URL}/api/echo/', headers=custom_headers)

    assert resp.status_code == 200
    echoed_headers = resp.json()['headers']
    assert echoed_headers.get('X-Test-Id') == 'pytest-exercise-4'
05

Follow a Redirect and Assert the Final URL

#

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
Solution
import requests

BASE_URL = 'https://apilearn.tukas.dev'


def test_redirect_followed():
    resp = requests.get(f'{BASE_URL}/api/redirect/')

    assert resp.status_code == 200
    assert len(resp.history) > 0
    assert 'apilearn.tukas.dev' in resp.url


def test_redirect_not_followed():
    resp = requests.get(f'{BASE_URL}/api/redirect/', allow_redirects=False)

    assert resp.status_code in (301, 302, 307, 308)
    assert 'Location' in resp.headers