Python · Тестування з pytest · Середній

Тестування HTTP API

5 завдань

Пишіть автоматизовані тести для HTTP API за допомогою requests та фікстур pytest.

Тестування HTTP API з pytest

#
## Від ручного до автоматизованого тестування API Якщо ви тестували API за допомогою REST-клієнта, ви знаєте цей цикл: надіслати запит, подивитися на відповідь, рухатися далі. Автоматизовані API-тести роблять те саме -- але повторювано, за мілісекунди, і вони повідомляють вас, щойно щось регресує. З pytest і бібліотекою `requests` API-тест виглядає майже ідентично до ручної перевірки: ```python import requests def test_products_list(): resp = requests.get('https://apilearn.tukas.dev/api/products/') assert resp.status_code == 200 ``` Різниця: це запускається автоматично кожного разу, коли ви пушите, без участі людини. ## Що перевіряти в API-тесті Перевіряйте пошарово -- кожен шар будується на попередньому: 1. **Код статусу** -- чи відповідає взагалі сервер, і чи правильним кодом? 2. **Структура JSON** -- чи є у відповіді очікувані ключі? 3. **Значення** -- чи правильні конкретні значення для цього вхідного запиту? ```python def test_products_list(): resp = requests.get('https://apilearn.tukas.dev/api/products/') assert resp.status_code == 200 # шар 1: статус data = resp.json() assert 'results' in data # шар 2: структура assert 'count' in data assert isinstance(data['results'], list) # шар 2: тип assert len(data['results']) > 0 # шар 3: значення ``` Починайте з коду статусу. Якщо він неправильний, решта -- шум: тіло відповіді може бути сторінкою помилки, а не JSON. ## BASE_URL: одне місце для змін Визначте базовий URL API один раз і посилайтеся на нього скрізь. Це робить перемикання між середовищами (staging, local, production) однорядковою зміною: ```python BASE_URL = 'https://apilearn.tukas.dev' def test_products(): resp = requests.get(f'{BASE_URL}/api/products/') assert resp.status_code == 200 ``` Ще краще -- помістіть його у `conftest.py` як фікстуру: тоді всі тестові файли спільно його використовують і ви можете перевизначати його на рівні директорії. ## Автентифікація: виносьте у фікстуру Повторення процесу входу в кожному тесті -- тендітний підхід. Винесіть його у фікстуру: ```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}'} ``` Будь-який тест, що потребує автентифікації, приймає `auth_headers` як параметр -- pytest впроваджує його: ```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 ``` Оголошення `auth_token` session-scoped означає, що виклик входу відбувається один раз за запуск тестів, а не один раз за тест. ## Проектування ізольованих, відтворюваних тестів Хороший API-тест: - **Не залежить від порядку виконання тестів** -- або читає публічні/стабільні дані, або налаштовує власний стан - **Не залишає побічних ефектів**, що ламають інші тести -- використовуйте yield-фікстури для очищення всього, що ви створюєте - **Перевіряє конкретно** -- перевіряйте конкретні значення, а не просто те, що відповідь непорожня Для read-only ендпоінтів (GET) тести природно ізольовані. Для операцій запису -- очищайте через yield-фікстуру: ```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 -- що ви отримуєте у відповідь ```python resp = requests.get(url, headers={}, params={}) resp.status_code # int: 200, 201, 404, 500 тощо resp.json() # dict або list -- розібране тіло JSON resp.text # str -- сире тіло відповіді resp.headers # dict-подібний об'єкт -- заголовки відповіді (ключі без урахування регістру) resp.url # str -- фінальний URL після будь-яких редиректів resp.history # list -- проміжні відповіді в ланцюжку редиректів resp.raise_for_status() # підіймає requests.HTTPError, якщо status_code >= 400 ```

Тести API для apilearn.tukas.dev

#
## Спільне налаштування у 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 -- статус, структура, значення ```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 -- вхід та захищені ендпоінти ```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 і 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 за замовчуванням автоматично переходить за редиректами 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 ``` ## Точна перевірка вмісту відповіді ```python def test_product_fields(base_url): resp = requests.get(f'{base_url}/api/products/') products = resp.json()['results'] # Перевірка структури першого елемента 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

#
**Загальний шаблон перевірок:** ```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:** | Властивість | Тип | Опис | |-------------|-----|------| | `.status_code` | int | HTTP-статус (200, 201, 401, 404...) | | `.json()` | dict/list | Розібране тіло JSON | | `.text` | str | Сире тіло відповіді | | `.headers` | dict-подібний | Заголовки відповіді | | `.url` | str | Фінальний URL після редиректів | | `.history` | list | Ланцюжок редиректів | | `.raise_for_status()` | -- | Підіймає `HTTPError`, якщо статус >= 400 | **HTTP-методи:** ```python requests.get(url, params={}, headers={}) requests.post(url, json={}, headers={}) # тіло JSON requests.post(url, data={}, headers={}) # form-encoded тіло requests.put(url, json={}, headers={}) requests.delete(url, headers={}) ``` **Фікстура заголовка авторизації (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}'} ``` **Управління редиректами:** ```python requests.get(url) # переходить за редиректами (за замовчуванням) requests.get(url, allow_redirects=False) # зупиняється на першому редиректі ```
01

Тест GET /api/products/ -- статус і структура

#

Напишіть тест, що викликає `GET https://apilearn.tukas.dev/api/products/` і перевіряє: (1) код статусу 200, (2) JSON-відповідь містить ключ `"results"`, (3) `"results"` є списком, (4) список непорожній. Запустіть `pytest -v` і переконайтеся, що тест проходить.

import requests

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


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

    # перевірте код статусу 200
    # перевірте наявність 'results' у JSON-відповіді
    # перевірте, що results є списком
    # перевірте, що results непорожній
    pass
Рішення
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

Тест пагінації -- точна кількість елементів

#

Напишіть тест, що викликає GET https://apilearn.tukas.dev/api/products/ з параметрами запиту page=1 і page_size=3. Перевірте, що код статусу відповіді 200 і що len(data["results"]) == 3. Використовуйте аргумент `params` в `requests.get` для передачі параметрів запиту -- не будуйте рядок запиту вручну.

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}
    )

    # перевірте статус 200
    # перевірте рівно 3 результати
    pass
Рішення
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

Фікстура заголовка авторизації та захищений ендпоінт

#

Напишіть conftest.py з: (1) session-scoped фікстурою `auth_token`, що відправляє POST на https://apilearn.tukas.dev/api/auth/token/ з username="testuser" і password="TestUser2024!" та повертає рядок токену; (2) function-scoped фікстурою `auth_headers`, що повертає {"Authorization": "Token <token>"}. Потім напишіть тест, що використовує `auth_headers` для виклику GET https://apilearn.tukas.dev/api/users/profile/ і перевіряє статус 200 та наявність "username" у відповіді.

# conftest.py
import pytest
import requests

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

@pytest.fixture(scope='session')
def auth_token():
    # POST на BASE_URL/api/auth/token/ з dict облікових даних
    # поверніть resp.json()['token']
    pass

@pytest.fixture
def auth_headers(auth_token):
    # поверніть {'Authorization': f'Token {auth_token}'}
    pass


# test_profile.py
def test_profile_authenticated(auth_headers):
    # GET BASE_URL/api/users/profile/ з auth_headers
    # перевірте статус 200
    # перевірте наявність 'username' у JSON-відповіді
    pass
Рішення
# 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

Надсилання власного заголовка та перевірка його відлуння

#

Напишіть тест, що надсилає GET https://apilearn.tukas.dev/api/echo/ з власним заголовком X-Test-Id: pytest-exercise-4. Перевірте, що код статусу відповіді 200 і що тіло JSON відображає ваш заголовок назад -- ендпоінт echo повертає заголовки запиту під ключем "headers" у 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)

    # перевірте статус 200
    # перевірте resp.json()['headers']['X-Test-Id'] == 'pytest-exercise-4'
    pass
Рішення
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

Перехід за редиректом та перевірка фінального URL

#

Напишіть два тести для GET https://apilearn.tukas.dev/api/redirect/. Перший: дозвольте requests переходити за редиректом (за замовчуванням) -- перевірте статус 200, що resp.history непорожній, і що фінальний resp.url містить "apilearn.tukas.dev". Другий: передайте allow_redirects=False -- перевірте, що статус є 3xx-кодом (301, 302, 307 або 308) і що у відповіді присутній заголовок "Location".

import requests

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


def test_redirect_followed():
    resp = requests.get(f'{BASE_URL}/api/redirect/')
    # перевірте статус 200
    # перевірте, що resp.history непорожній
    # перевірте наявність 'apilearn.tukas.dev' у resp.url
    pass


def test_redirect_not_followed():
    resp = requests.get(f'{BASE_URL}/api/redirect/', allow_redirects=False)
    # перевірте, що статус є 3xx
    # перевірте наявність 'Location' у resp.headers
    pass
Рішення
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