Modern QA2026Основы HTTP
Join

Course12 Programming for QA

Foundations · Chapter 12

Основы HTTP

Updated Jul 2026

Каждый API-тест, каждый шаг автоматизации браузера и каждая сессия отладки на сетевом уровне зависят от понимания HTTP. Это не теория — это протокол, на котором ваши тесты общаются каждый раз при взаимодействии с веб-приложением.

Методы HTTP

Метод Назначение Идемпотентный? Безопасный?
GET Получить ресурс Да Да
POST Создать ресурс Нет Нет
PUT Полностью заменить ресурс Да Нет
PATCH Частичное обновление Нет Нет
DELETE Удалить ресурс Да Нет
HEAD Как GET, но без тела ответа Да Да
OPTIONS Узнать допустимые методы Да Да

Что означает идемпотентность для тестирования

Идемпотентный запрос даёт одинаковый результат, независимо от того, отправите вы его один или десять раз. Это важно для логики повторных попыток и надёжности тестов:

  • PUT /users/123 {"name": "Alice"} — отправьте 10 раз, пользователь по-прежнему называется "Alice"
  • POST /users {"name": "Alice"} — отправьте 10 раз, вы можете получить 10 пользователей (или 409 Conflict, если email уникален)
  • DELETE /users/123 — первый вызов удаляет пользователя, последующие возвращают 404 (но состояние то же: пользователь удалён)

Что тестировать для каждого метода

# GET: verify response content and caching
def test_get_user(api):
    r = api.get("/users/1")
    assert r.status_code == 200
    assert r.headers["Content-Type"] == "application/json"
    assert "id" in r.json()

# POST: verify creation and response
def test_create_user(api):
    r = api.post("/users", json={"name": "Alice", "email": "alice@test.com"})
    assert r.status_code == 201
    assert "id" in r.json()
    # Verify the user actually exists
    get_r = api.get(f"/users/{r.json()['id']}")
    assert get_r.status_code == 200

# PUT: verify full replacement
def test_update_user(api):
    r = api.put("/users/1", json={"name": "Bob", "email": "bob@test.com"})
    assert r.status_code == 200
    # Verify ALL fields are what you sent (PUT replaces everything)
    user = api.get("/users/1").json()
    assert user["name"] == "Bob"

# DELETE: verify removal
def test_delete_user(api):
    r = api.delete("/users/1")
    assert r.status_code == 204
    # Verify the user is gone
    get_r = api.get("/users/1")
    assert get_r.status_code == 404

Коды состояния HTTP

Диапазон Значение Типичные коды
2xx Успех 200 OK, 201 Created, 204 No Content
3xx Перенаправление 301 Moved Permanently, 302 Found, 304 Not Modified
4xx Ошибка клиента 400, 401, 403, 404, 409, 422, 429
5xx Ошибка сервера 500, 502, 503

Коды состояния, которые должен знать QA-инженер

Код Значение Тестовый сценарий
200 OK Успешный GET, PUT, PATCH
201 Created Успешный POST
204 No Content Успешный DELETE
301 Moved Permanently Старый URL должен перенаправлять на новый
304 Not Modified Кеширование: ресурс не изменился с последнего запроса
400 Bad Request Некорректный JSON, отсутствующие обязательные поля
401 Unauthorized Отсутствующий или истёкший токен аутентификации
403 Forbidden Валидный токен, но недостаточно прав
404 Not Found Несуществующий ресурс
405 Method Not Allowed POST на эндпоинт, принимающий только GET
409 Conflict Дублирующее создание (ограничение уникальности)
422 Unprocessable Entity Валидный JSON, но невалидные данные (например, формат email)
429 Too Many Requests Превышен лимит запросов
500 Internal Server Error Необработанное исключение на сервере
502 Bad Gateway Бэкенд-сервис недоступен
503 Service Unavailable Сервер перегружен или на обслуживании

Заголовки HTTP

Заголовки несут метаданные о запросе и ответе. Несколько заголовков критичны для тестирования.

Заголовки запроса для установки

Заголовок Назначение Пример
Content-Type Формат тела запроса application/json
Authorization Учётные данные аутентификации Bearer eyJhbG...
Accept Желаемый формат ответа application/json
User-Agent Идентификация клиента Пользовательское значение для идентификации тестов

Заголовки ответа для проверки

Заголовок Что тестировать
Content-Type Соответствует ожидаемому формату (неправильный тип вызывает скрытые сбои)
Set-Cookie Проверка атрибутов HttpOnly, Secure, SameSite
Access-Control-Allow-Origin Корректная конфигурация CORS для разрешённых доменов
Cache-Control Подходящее кеширование для типа ресурса
X-Request-ID Присутствие для трассируемости (полезно при отладке)
Retry-After Присутствие в ответах 429
Location Присутствие в 201 (указывает на новый ресурс) и при перенаправлениях 3xx
def test_security_headers(api):
    r = api.get("/")
    # Security headers should be present
    assert "X-Content-Type-Options" in r.headers
    assert r.headers["X-Content-Type-Options"] == "nosniff"
    assert "X-Frame-Options" in r.headers
    assert "Strict-Transport-Security" in r.headers

def test_cors_headers(api):
    r = api.options("/api/users", headers={
        "Origin": "https://app.example.com",
        "Access-Control-Request-Method": "GET"
    })
    assert r.headers["Access-Control-Allow-Origin"] in [
        "https://app.example.com", "*"
    ]

Цикл запроса/ответа

Client                                  Server
  |                                       |
  |-- HTTP Request ---------------------->|
  |   Method: POST                        |
  |   URL: /api/users                     |
  |   Headers:                            |
  |     Content-Type: application/json    |
  |     Authorization: Bearer token       |
  |   Body: {"name": "Alice"}             |
  |                                       |
  |<-- HTTP Response --------------------|
  |   Status: 201 Created                |
  |   Headers:                           |
  |     Content-Type: application/json   |
  |     Location: /api/users/42          |
  |   Body: {"id": 42, "name": "Alice"} |

Что проверять на каждом уровне

  1. Код состояния: сервер корректно принял/отклонил запрос?
  2. Заголовки ответа: присутствуют ли заголовки безопасности? Корректен ли тип содержимого?
  3. Тело ответа: данные соответствуют ожиданиям? Все ли поля присутствуют?
  4. Время ответа: укладывается ли ответ в допустимую задержку?
  5. Побочные эффекты: сервер действительно создал/обновил/удалил ресурс?

Параметры запроса и тело запроса

Аспект Параметры запроса Тело запроса
Используется с GET, DELETE POST, PUT, PATCH
Видимо в URL Да Нет
Кешируемо Да (часть URL) Нет
Ограничение размера ~2КБ (ограничение длины URL) Без практического ограничения
Пример GET /users?page=1&limit=10 POST /users {"name": "Alice"}
# Query parameters
r = requests.get(f"{base_url}/users", params={"page": 1, "limit": 10, "sort": "name"})
# Results in: GET /users?page=1&limit=10&sort=name

# Request body
r = requests.post(f"{base_url}/users", json={"name": "Alice", "email": "alice@test.com"})

Типы содержимого

Content-Type Используется для Пример
application/json REST API {"name": "Alice"}
application/x-www-form-urlencoded HTML-формы name=Alice&email=alice@test.com
multipart/form-data Загрузка файлов Бинарные данные файла с разделителями
text/html Веб-страницы HTML-содержимое
application/xml SOAP API, устаревшие системы <user><name>Alice</name></user>
# Sending form data (not JSON)
r = requests.post(f"{base_url}/login", data={"username": "alice", "password": "pass123"})

# Uploading a file
with open("document.pdf", "rb") as f:
    r = requests.post(f"{base_url}/upload", files={"file": f})

Практическое упражнение

  1. Используйте curl или requests для выполнения запросов GET, POST, PUT и DELETE к публичному API (например, JSONPlaceholder)
  2. Проверьте заголовки ответа для каждого запроса и верифицируйте Content-Type, коды состояния и заголовки безопасности
  3. Напишите тест, проверяющий корректную обработку 401: отправьте запрос без аутентификации, с истёкшей аутентификацией и с невалидной аутентификацией
  4. Напишите тест, проверяющий корректные заголовки CORS для кросс-доменного запроса

Ключевые выводы

  • Знайте все методы HTTP и их характеристики идемпотентности
  • Тестируйте как успешные коды состояния, так и коды ошибок (особенно 400, 401, 403, 404, 409, 429)
  • Проверяйте заголовки ответа, а не только тело — заголовки безопасности, CORS, кеширование
  • Понимайте разницу между параметрами запроса (GET) и телом запроса (POST/PUT)
  • Несоответствие Content-Type вызывает скрытые сбои — всегда проверяйте