## От ручного к автоматизированному тестированию 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
```
Session scope для `auth_token` означает, что вызов логина происходит один раз за прогон тестов, а не один раз за тест.
## Проектирование изолированных, повторяемых тестов
Хороший 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
```
Напишите тест, который вызывает `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
Напишите тест, вызывающий GET `https://apilearn.tukas.dev/api/products/` с параметрами запроса `page=1` и `page_size=3`. Проверьте, что статус ответа 200 и `len(data["results"]) == 3`. Используйте именованный аргумент `params` в `requests.get` для передачи параметров запроса -- не стройте строку запроса вручную.
Напишите `conftest.py` с: (1) фикстурой `auth_token` с session scope, которая делает POST на `https://apilearn.tukas.dev/api/auth/token/` с `username="testuser"` и `password="TestUser2024!"` и возвращает строку токена; (2) фикстурой `auth_headers` с function scope, возвращающей `{"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/ со словарём учётных данных
# вернуть 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
Напишите тест, отправляющий `GET https://apilearn.tukas.dev/api/echo/` с пользовательским заголовком `X-Test-Id: pytest-exercise-4`. Проверьте, что статус ответа 200 и что тело JSON отражает ваш заголовок обратно -- эхо-эндпоинт возвращает заголовки запроса в ключе `"headers"` JSON-ответа.
Напишите два теста для `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
Мы используем необходимые cookies для работы сайта. С вашего разрешения мы также можем сохранять настройки сайта и использовать аналитические и рекламные cookies, чтобы понимать использование сайта и поддерживать развитие проекта.
* Вы всегда можете изменить свой выбор в настройках сайта.
Выберите категории cookies
Настройки аналитики
Можно отключить аналитику использования платформы. Также можно отправить в Google Analytics запрос на удаление данных об использовании этого сайта, связанных с этим браузером.
Учебный workspace
Учитесь, читая, запуская код и решая задачи.
Практикуйте программирование с пояснениями тем, упражнениями, инструментами browser IDE, проверкой regex и тренировкой печати кода в одном workspace.
Открывайте инструменты во вкладках.Упражнения, IDE-инструменты и тренажеры остаются доступными как вкладки сайта.
Переключайтесь без потери контекста.Переходите между пояснениями, кодом и утилитами, сохраняя свое место.
Используйте sidebar как карту.Левые панели содержат навигацию, настройки, файлы, libraries и управление инструментами.
PythonJavaScriptSQLite
Одна IDE, три практичных режима
Python в браузере.Запускайте небольшие скрипты, пробуйте библиотеки и тренируйте API-запросы без установки.
JavaScript для быстрых экспериментов.Проверяйте код для браузера и сравнивайте идеи рядом с учебными материалами.
SQLite для практики с данными.Открывайте обозреватель базы данных, изучайте таблицы, пишите запросы и учитесь SQL локально.
ТемаIDE
Работайте рядом в split tabs
Держите инструкции перед глазами.Откройте упражнение или справочную страницу рядом с IDE, вместо постоянных переключений.
Сравнивайте инструменты во время обучения.Размещайте проверки regex, пояснения и эксперименты с кодом рядом, когда это нужно для задачи.
Закройте split, когда закончите.Workspace вернётся к одной сфокусированной вкладке, а открытые вкладки сайта останутся доступны.
Тренажер слепой печати кода
Или просто текста
Тренажер рассчитан на физическую клавиатуру.Откройте этот раздел на ноутбуке или компьютере с широким экраном. На телефоне тренировка слепой печати не будет корректной.
Скорость: 0 зн/мин
0 слов/мин
Лучшая скорость (60с): 0 зн/мин
0 слов/мин
Ошибки: 0
Общее время: 0.0 с
Для активации режима слепого набора не подсматривайте на физическую клавиатуру.