Версионирование API
Updated Jul 2026
Версионирование API — это способ, которым команды развивают свои API без нарушения работы существующих клиентов. Как QA-инженер, вы должны тестировать, что новые версии работают корректно И что старые версии продолжают функционировать. Нарушение обратной совместимости — один из наиболее критичных багов, которые вы можете обнаружить.
Стратегии версионирования
| Стратегия | Пример | Плюсы | Минусы |
|---|---|---|---|
| URL-путь | /v1/users, /v2/users |
Наглядно, легко маршрутизировать | Засорение URL, сложно удалять старые версии |
| Заголовок | Accept: application/vnd.api+json; version=2 |
Чистые URL | Сложнее тестировать вручную, легко пропустить |
| Query-параметр | /users?version=2 |
Просто | Засорение URL, сложности с кешированием |
| Согласование контента | Accept: application/vnd.company.v2+json |
RESTful | Сложно реализовать и тестировать |
Версионирование через URL-путь (наиболее распространённое)
def test_v1_users_endpoint(api):
r = api.get("/v1/users")
assert r.status_code == 200
user = r.json()["items"][0]
assert "name" in user # v1 returns "name"
def test_v2_users_endpoint(api):
r = api.get("/v2/users")
assert r.status_code == 200
user = r.json()["items"][0]
assert "first_name" in user # v2 splits into first/last
assert "last_name" in user
Версионирование через заголовок
def test_header_versioning(api, base_url):
# Default version (no header)
r = api.get("/users")
assert r.status_code == 200
default_fields = set(r.json()["items"][0].keys())
# Explicit v2
r = api.get("/users", headers={"API-Version": "2"})
assert r.status_code == 200
v2_fields = set(r.json()["items"][0].keys())
# v2 should have new fields
assert "first_name" in v2_fields
assert "last_name" in v2_fields
def test_missing_version_header_uses_default(api):
"""When no version header is sent, the API should use the default version."""
r = api.get("/users")
assert r.status_code == 200
# Verify it returns the default version's response format
Тестирование обратной совместимости
Наиболее важный аспект тестирования версионирования API: старые клиенты не должны ломаться при выпуске новых версий.
def test_v1_still_works_after_v2_launch(api):
"""v1 endpoints must continue to function correctly."""
# Create a user via v1
r = api.post("/v1/users", json={"name": "Alice", "email": "alice@test.com"})
assert r.status_code == 201
# Read via v1
user_id = r.json()["id"]
r = api.get(f"/v1/users/{user_id}")
assert r.status_code == 200
assert r.json()["name"] == "Alice"
# Delete via v1
r = api.delete(f"/v1/users/{user_id}")
assert r.status_code == 204
def test_v1_data_accessible_via_v2(api):
"""Data created in v1 should be accessible in v2 format."""
# Create in v1 format
r = api.post("/v1/users", json={"name": "Bob Smith", "email": "bob@test.com"})
user_id = r.json()["id"]
# Read in v2 format
r = api.get(f"/v2/users/{user_id}")
assert r.status_code == 200
# v2 should correctly split the name
assert r.json()["first_name"] == "Bob"
assert r.json()["last_name"] == "Smith"
def test_v2_data_accessible_via_v1(api):
"""Data created in v2 should be accessible in v1 format."""
r = api.post("/v2/users", json={
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@test.com"
})
user_id = r.json()["id"]
# Read in v1 format
r = api.get(f"/v1/users/{user_id}")
assert r.status_code == 200
assert r.json()["name"] == "Jane Doe" # v1 should combine first/last
Тестирование устаревших эндпоинтов
def test_deprecated_endpoint_returns_warning(api):
"""Deprecated endpoints should include a deprecation warning header."""
r = api.get("/v1/legacy-endpoint")
assert r.status_code == 200 # Still works
# Should include deprecation warning
assert "Deprecation" in r.headers or "Sunset" in r.headers or \
"X-Deprecated" in r.headers
def test_deprecated_endpoint_still_functional(api):
"""Deprecated endpoints must still work until officially removed."""
r = api.get("/v1/legacy-endpoint")
assert r.status_code == 200
assert len(r.json()["items"]) > 0 # Still returns data
Типичные баги версионирования
| Баг | Стратегия тестирования |
|---|---|
| v1 ломается после деплоя v2 | Запустите полный набор тестов v1 после деплоя v2 |
| Данные, созданные в v1, не видны в v2 | Кросс-версионные CRUD-тесты |
| Версия по умолчанию неожиданно меняется | Тестируйте запросы без явного указания версии |
| Ответы об ошибках различаются между версиями | Проверьте, что формат ошибок консистентен |
| Устаревший эндпоинт перестаёт работать до даты sunset | Мониторьте устаревшие эндпоинты |
| v2 возвращает поля формата v1 | Строго проверяйте схему ответа v2 |
Обнаружение версий
def test_api_exposes_available_versions(api, base_url):
"""API should document available versions."""
r = api.get("/")
if r.status_code == 200:
data = r.json()
# Some APIs list available versions at the root
if "versions" in data:
assert "v1" in data["versions"]
assert "v2" in data["versions"]
def test_unsupported_version_returns_error(api, base_url):
"""Requesting a non-existent version should return a clear error."""
r = api.get("/v99/users")
assert r.status_code in (400, 404)
Практическое упражнение
- Напишите тесты обратной совместимости: создайте ресурсы в v1, проверьте доступность в v2
- Напишите кросс-версионные тесты данных: создайте в v2, прочитайте в v1
- Протестируйте поведение версии по умолчанию (без указания версии)
- Протестируйте устаревшие эндпоинты: проверьте, что они работают и включают заголовки устаревания
- Напишите тестовую матрицу, покрывающую все CRUD-операции для всех версий API
Ключевые выводы
- Всегда тестируйте обратную совместимость: v1 не должен ломаться при запуске v2
- Тестируйте кросс-версионный доступ к данным: данные, созданные в одной версии, должны быть читаемы в другой
- Тестируйте поведение версии по умолчанию: что происходит, когда версия не указана?
- Мониторьте устаревшие эндпоинты: они должны работать до объявленной даты sunset
- Версионирование через URL-путь наиболее распространено; версионирование через заголовок чище, но сложнее тестировать вручную
- Тестирование совместимости версий предотвращает один из наиболее критичных типов API-багов
Тезис для собеседования: «Я строю наборы API-тестов в pytest с библиотекой requests, организованные вокруг аутентификации, CRUD-операций, обработки ошибок и граничных случаев, таких как ограничение частоты запросов и пагинация. Я тестирую как успешные пути, так и режимы отказа — просроченные токены, некорректные payload, утечку информации в ответах об ошибках. Я параметризую окружения, чтобы один и тот же набор тестов работал в dev, staging и production при изменении одной переменной.»