Проверка системы: 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-а:
- Щелчок по Endpoint-у (например,
POST /students) открывает его. - Try it out → поля открываются для редактирования.
- Заполните параметры и/или отредактируйте Request body (там уже есть пример JSON, построенный по модели!).
- Execute → на сервер отправляется настоящий запрос.
- Ниже: 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 показывает ещё и строку состояния — именно её вы и проверяете.
В 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 — создание студента шаг за шагом:
- Запустите сервер (
uvicorn main:app --reload) и откройте/docs. - POST /students → Try it out. Request body показывает:
{
"name": "string",
"age": 0,
"email": "string"
}- Замените на:
{
"name": "Tom",
"age": 27,
"email": "tom@mail.com"
}- Execute. Результат:
Code: 201
Response body:
{
"id": 3,
"name": "Tom",
"age": 27,
"email": "tom@mail.com"
}- Нажмите 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» в POST | status_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 | Дублирующийся email | POST /students с email Даны | 409 |
| 3 | Слишком молод | POST /students с возрастом 7 | 400 |
| 4 | Отсутствуют поля | POST /students только с именем | 422 |
| 5 | Существующий студент | GET /students/1 | 200 |
| 6 | Несуществующий студент | GET /students/99 | 404 |
| 7 | Нечисловой id | GET /students/abc | 422 |
| 8 | Удаление | DELETE /students/1 | 204 |
| 9 | Повторное удаление | DELETE /students/1 | 404 |
Первая проверка проходит всегда — большинство багов в случаях сбоя: 404, который возвращается как 200, или 409, который возвращается как 500.
Упражнение 2: curl
Напишите команды curl для сервера, работающего на http://127.0.0.1:8000:
- Создание студента (Dana, 24, dana@mail.com) — с показом кода.
- Чтение студента 1.
- Удаление студента 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, Noneurlopenвыбрасывает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>- Что это значит?
- Как правильно запускать проверки?
- Почему стоит начинать с пустой базы?
Показать решение
- Адрес и порт никто не слушает — соединение отклонено. Скрипт — это клиент, и ему нужен работающий сервер.
- Два терминала: в одном запустите
uvicorn main:app, а в другом — проверочный скрипт. - Чтобы результаты были предсказуемыми: 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. Страховочная сетка для каждого изменения.
- План проверок = «счастливый путь» + каждый случай сбоя из таблицы соответствий. Проверяйте коды и тело.
- Запускайте проверки на пустой базе и с работающим сервером.