Проверка системы: Swagger UI, curl и проверочный скрипт на Python

Цели урока
  • Проверять каждый Endpoint в Swagger UI — Try it out, тело, Execute и чтение ответа
  • Отправлять запросы POST, PUT и DELETE через curl
  • Написать скрипт на Python, который выполняет последовательность запросов и проверяет, что каждый код состояния — ожидаемый
  • Составить список ручных проверок (Test Plan) для всех действий CRUD и граничных случаев

Что мы изучим?

Сервер, который не проверен, — сервер, в работе которого нельзя быть уверенным. Мы изучим три способа — от ручного к автоматическому:

ИнструментКогда
Swagger UI (/docs)Удобная ручная проверка во время разработки
curlБыстрая проверка из терминала и привычка людей бэкенда и DevOps
Скрипт на PythonСерия проверок, которая запускается одним щелчком и проверяет всё, каждый раз

И план проверок (Test Plan): что вы проверяете и что ожидаете получить.

Зачем это нужно?

Каждое изменение в коде может сломать то, что работало. Если проверка — «я открыл браузер, и вроде всё нормально», баги дойдут до пользователей. Фиксированный список проверок и особенно скрипт, который его выполняет, позволяют менять код уверенно. Это ещё и тренировка для интеграционного проекта, где целый день посвящён отладке и ручной проверке всех действий CRUD.

Объяснение с основ

Swagger UI

Swagger UI — страница, которую FastAPI строит автоматически по коду: по маршрутам, подсказкам типов и моделям Pydantic. Она показывает каждый Endpoint и позволяет отправлять настоящие запросы из браузера.

http://127.0.0.1:8000/docs

Процесс для каждого Endpoint-а:

  1. Щелчок по Endpoint-у (например, POST /students) открывает его.
  2. Try it out → поля открываются для редактирования.
  3. Заполните параметры и/или отредактируйте Request body (там уже есть пример JSON, построенный по модели!).
  4. Execute → на сервер отправляется настоящий запрос.
  5. Ниже: Curl (равносильная команда!), Request URL, Response body, Response code и Response headers.

Бонус: есть ещё http://127.0.0.1:8000/openapi.json. Это описание API в стандартном формате (OpenAPI), по которому Swagger строит страницу. Другие инструменты тоже умеют его читать.

curl — напоминание

curl -i http://127.0.0.1:8000/students
curl -i -X POST http://127.0.0.1:8000/students -H "Content-Type: application/json" -d '{"name": "Tom", "age": 27, "email": "tom@mail.com"}'
curl -i -X DELETE http://127.0.0.1:8000/students/3

-i показывает ещё и строку состояния — именно её вы и проверяете.

curl с JSON в PowerShell

В PowerShell кавычки внутри JSON — сложная история. Два удобных способа: (1) сохраните JSON в файл (new_student.json) и отправьте через curl.exe -i -X POST URL -H "Content-Type: application/json" -d "@new_student.json". (2) Скопируйте команду curl, которую показывает Swagger, и выполните её в Git Bash.

Проверочный скрипт на Python

Скрипт, который отправляет запросы на работающий сервер и для каждого проверяет: такой ли код, какой мы ожидали? В конце он печатает, сколько проверок прошло. Такая проверка, которая запускает систему снаружи, как настоящий клиент, называется сквозной (End-to-End). (Позже в треке вы изучите профессиональные инструменты тестирования, например pytest. Идея та же.)

План проверок — список проверок

Для каждого Endpoint-а: «счастливый путь» + каждый случай сбоя из таблицы соответствий. Если у каждой строки таблицы есть проверка, вы знаете, что контракт API соблюдается.

Синтаксис

import json
import urllib.error
import urllib.request

BASE_URL = "http://127.0.0.1:8000"

def call(method, path, body=None):
    data = json.dumps(body).encode() if body is not None else None
    request = urllib.request.Request(BASE_URL + path, data=data, method=method,
                                     headers={"Content-Type": "application/json"})
    try:
        response = urllib.request.urlopen(request)
        status, raw = response.status, response.read()
    except urllib.error.HTTPError as error:
        status, raw = error.code, error.read()
    return status, (json.loads(raw) if raw else None)

call возвращает tuple из (кода, тела ответа) — и при успехе, и при ошибке (HTTPError). Пустое тело (204) → None.

Первый пример кода

Swagger — создание студента шаг за шагом:

  1. Запустите сервер (uvicorn main:app --reload) и откройте /docs.
  2. POST /students → Try it out. Request body показывает:
{
  "name": "string",
  "age": 0,
  "email": "string"
}
  1. Замените на:
{
  "name": "Tom",
  "age": 27,
  "email": "tom@mail.com"
}
  1. Execute. Результат:
Code: 201
Response body:
{
  "id": 3,
  "name": "Tom",
  "age": 27,
  "email": "tom@mail.com"
}
  1. Нажмите Execute ещё раз с тем же телом. Теперь: 409, "a student with email tom@mail.com already exists".

Разбор кода построчно

Что вы видите в SwaggerОткуда это берётся в коде
Список всех Endpoint-овДекораторы @app.get/post/put/delete
Имя API вверху («Students API»)FastAPI(title="Students API")
Пример JSON в телеМодель StudentIn (поля и типы)
Поле student_id с integerПодсказка типа student_id: int
Код 201 как «Successful Response» в POSTstatus_code=201
id 3 в ответеdb.create_student → AUTO_INCREMENT

Всё выводится из кода. Когда вы меняете маршрут, документация обновляется сама. Поэтому она всегда точна.

Ещё один пример

Сквозной проверочный скрипт. smoke_test.py (сервер должен работать в другом терминале, на пустой таблице: в Database Client выполните DROP TABLE students; и перезапустите сервер — init_db создаст таблицу снова):

import json
import urllib.error
import urllib.request

BASE_URL = "http://127.0.0.1:8000"

def call(method, path, body=None):
    data = json.dumps(body).encode() if body is not None else None
    request = urllib.request.Request(BASE_URL + path, data=data, method=method,
                                     headers={"Content-Type": "application/json"})
    try:
        response = urllib.request.urlopen(request)
        status, raw = response.status, response.read()
    except urllib.error.HTTPError as error:
        status, raw = error.code, error.read()
    return status, (json.loads(raw) if raw else None)

CHECKS = [
    ("create Dana", "POST", "/students", {"name": "Dana", "age": 24, "email": "dana@mail.com"}, 201),
    ("create Avi", "POST", "/students", {"name": "Avi", "age": 31, "email": "avi@mail.com"}, 201),
    ("duplicate email", "POST", "/students", {"name": "Dana Copy", "age": 20, "email": "dana@mail.com"}, 409),
    ("too young", "POST", "/students", {"name": "Kid", "age": 7, "email": "kid@mail.com"}, 400),
    ("missing fields", "POST", "/students", {"name": "Noa"}, 422),
    ("list", "GET", "/students", None, 200),
    ("get existing", "GET", "/students/1", None, 200),
    ("get missing", "GET", "/students/99", None, 404),
    ("bad id", "GET", "/students/abc", None, 422),
    ("update", "PUT", "/students/2", {"name": "Avi Levi", "age": 32, "email": "avi@mail.com"}, 200),
    ("update missing", "PUT", "/students/99", {"name": "Ghost", "age": 30, "email": "g@mail.com"}, 404),
    ("update clash", "PUT", "/students/2", {"name": "Avi", "age": 32, "email": "dana@mail.com"}, 409),
    ("delete", "DELETE", "/students/1", None, 204),
    ("delete again", "DELETE", "/students/1", None, 404),
]

def main():
    passed = 0
    for name, method, path, body, expected in CHECKS:
        status, data = call(method, path, body)
        ok = status == expected
        passed = passed + (1 if ok else 0)
        mark = "PASS" if ok else "FAIL"
        print(mark, "|", name, "|", method, path, "| expected", expected, "got", status)
    print(passed, "/", len(CHECKS), "checks passed")

if __name__ == "__main__":
    main()

Вывод:

PASS | create Dana | POST /students | expected 201 got 201
PASS | create Avi | POST /students | expected 201 got 201
PASS | duplicate email | POST /students | expected 409 got 409
PASS | too young | POST /students | expected 400 got 400
PASS | missing fields | POST /students | expected 422 got 422
PASS | list | GET /students | expected 200 got 200
PASS | get existing | GET /students/1 | expected 200 got 200
PASS | get missing | GET /students/99 | expected 404 got 404
PASS | bad id | GET /students/abc | expected 422 got 422
PASS | update | PUT /students/2 | expected 200 got 200
PASS | update missing | PUT /students/99 | expected 404 got 404
PASS | update clash | PUT /students/2 | expected 409 got 409
PASS | delete | DELETE /students/1 | expected 204 got 204
PASS | delete again | DELETE /students/1 | expected 404 got 404
14 / 14 checks passed

Разбор:

  • CHECKS — список tuple: имя проверки, метод, путь, тело и ожидаемый код. Данные отделены от логики (разделение ответственности!). Добавить проверку = добавить строку.
  • Порядок важен: проверки зависят друг от друга (сначала создают, потом удаляют). Поэтому начинают с пустой базы.
  • Цикл — отправляет, сравнивает с expected и печатает PASS/FAIL.
  • Магия: вы меняете что-то в коде (рефакторинг), запускаете — и через секунду видите, не сломалось ли что-нибудь. Страховочная сетка.

Попробуйте сломать: в main.py поменяйте status_code=204 на 200 в DELETE и сохраните (--reload). Удалите таблицу (DROP TABLE students; в Database Client), перезапустите и выполните скрипт:

FAIL | delete | DELETE /students/1 | expected 204 got 200
13 / 14 checks passed

Скрипт поймал изменение. Верните код обратно.

Типичная ошибка

Ошибка 1: проверять только «счастливый путь». «Я создал студента, и это сработало» ≠ «сервер работает». Большинство багов в серверном коде — в случаях сбоя: 404, который возвращается как 200, 409, который возвращается как 500. План проверок = «счастливый путь» + каждая строка сбоя из таблицы соответствий.

Ошибка 2: запускать проверочный скрипт на базе с прежними данными. «create Dana» упадёт с 409, потому что Дана уже есть с прошлого запуска. Пустая база перед каждым запуском (или, позже в треке, отдельная проверочная база).

Ошибка 3: забыть, что сервер должен работать.

urllib.error.URLError: <urlopen error [WinError 10061] No connection could be made because the target machine actively refused it>

«Connection refused» = порт никто не слушает (вспомните тему сетей!). Запустите uvicorn в отдельном терминале.

Ошибка 4: проверять только код, а не тело. Код 200 с неправильным телом (имя не обновилось) — всё равно баг. В важных проверках проверяйте и значения: data["name"] == "Avi Levi". (См. самостоятельную практику.)

Практика

Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.

Упражнение 1: План проверок (Test Plan)

Составьте план проверок для API студентов: не меньше 8 проверок с методом, путём, тем, что вы отправляете, и кодом, который ожидаете получить. Включите «счастливый путь» и случаи сбоя.

Показать решение
#ПроверкаЗапросОжидаемый код
1Создание ДаныPOST /students с допустимым телом201
2Дублирующийся emailPOST /students с email Даны409
3Слишком молодPOST /students с возрастом 7400
4Отсутствуют поляPOST /students только с именем422
5Существующий студентGET /students/1200
6Несуществующий студентGET /students/99404
7Нечисловой idGET /students/abc422
8УдалениеDELETE /students/1204
9Повторное удалениеDELETE /students/1404

Первая проверка проходит всегда — большинство багов в случаях сбоя: 404, который возвращается как 200, или 409, который возвращается как 500.

Упражнение 2: curl

Напишите команды curl для сервера, работающего на http://127.0.0.1:8000:

  1. Создание студента (Dana, 24, dana@mail.com) — с показом кода.
  2. Чтение студента 1.
  3. Удаление студента 1 — с показом кода.
Показать решение
curl -i -X POST http://127.0.0.1:8000/students -H "Content-Type: application/json" -d '{"name": "Dana", "age": 24, "email": "dana@mail.com"}'
curl http://127.0.0.1:8000/students/1
curl -i -X DELETE http://127.0.0.1:8000/students/1

-i показывает код состояния — при проверке это самое важное. В Swagger (/docs) те же запросы можно выполнить из браузера.

Упражнение 3: Функция call

Напишите функцию call(method, path, body=None), которая отправляет запрос на сервер с помощью urllib.request и возвращает tuple из (кода, тела в виде словаря или None). Ошибки HTTP (4xx, 5xx) не должны приводить к падению — они тоже возвращают код.

Показать решение
import json
import urllib.error
import urllib.request

BASE_URL = "http://127.0.0.1:8000"


def call(method, path, body=None):
    data = None
    if body is not None:
        data = json.dumps(body).encode()
    request = urllib.request.Request(BASE_URL + path, data=data, method=method,
                                     headers={"Content-Type": "application/json"})
    try:
        response = urllib.request.urlopen(request)
        status, raw = response.status, response.read()
    except urllib.error.HTTPError as error:
        status, raw = error.code, error.read()
    if raw:
        return status, json.loads(raw)
    return status, None
  • urlopen выбрасывает HTTPError при 4xx и 5xx — а при проверке нас интересуют именно эти ответы, поэтому их ловят и возвращают код.
  • У ответа 204 нет тела — поэтому None.

Упражнение 4: Скрипт, который печатает PASS / FAIL

Напишите часть, которая выполняет список проверок и печатает результат. Чтобы потренироваться без сервера, результаты уже есть в списке: имя проверки, ожидаемый код и полученный код.

results = [
    ("create Dana", 201, 201),
    ("duplicate email", 409, 409),
    ("get missing", 404, 200),
    ("delete", 204, 204),
]

Ожидаемый вывод:

PASS | create Dana | expected 201 got 201
PASS | duplicate email | expected 409 got 409
FAIL | get missing | expected 404 got 200
PASS | delete | expected 204 got 204
3 / 4 checks passed
Показать решение
results = [
    ("create Dana", 201, 201),
    ("duplicate email", 409, 409),
    ("get missing", 404, 200),
    ("delete", 204, 204),
]

passed = 0
for name, expected, got in results:
    if expected == got:
        status = "PASS"
        passed = passed + 1
    else:
        status = "FAIL"
    print(status, "|", name, "| expected", expected, "got", got)
print(passed, "/", len(results), "checks passed")
PASS | create Dana | expected 201 got 201
PASS | duplicate email | expected 409 got 409
FAIL | get missing | expected 404 got 200
PASS | delete | expected 204 got 204
3 / 4 checks passed

С настоящим сервером got приходит из call(...). FAIL здесь выявляет настоящий баг: несуществующий студент возвращается как 200 вместо 404.

Упражнение 5: Сервер не запущен

Вы запускаете проверочный скрипт и получаете:

urllib.error.URLError: <urlopen error [WinError 10061] No connection could be made
because the target machine actively refused it>
  1. Что это значит?
  2. Как правильно запускать проверки?
  3. Почему стоит начинать с пустой базы?
Показать решение
  1. Адрес и порт никто не слушает — соединение отклонено. Скрипт — это клиент, и ему нужен работающий сервер.
  2. Два терминала: в одном запустите uvicorn main:app, а в другом — проверочный скрипт.
  3. Чтобы результаты были предсказуемыми: id первого студента — 1, а список начинается пустым. На базе со старыми данными «создание Даны» может вернуть 409, потому что она уже есть, — и проверка не пройдёт, хотя бага нет.

Итоги

  • Swagger UI (/docs): Try it out → тело → Execute. Строится автоматически по коду и показывает равносильную команду curl.
  • curl -i: быстрая проверка из терминала. В PowerShell — JSON из файла (-d "@file.json") или Git Bash.
  • Скрипт на Python (End-to-End): список проверок (данные) + цикл (логика) → PASS/FAIL. Страховочная сетка для каждого изменения.
  • План проверок = «счастливый путь» + каждый случай сбоя из таблицы соответствий. Проверяйте коды и тело.
  • Запускайте проверки на пустой базе и с работающим сервером.

Официальная документация