


Тестирование API в Postman начинается с понятного контракта: что отправляем, какой ответ ожидаем и как проверяем сохранённое состояние. Соберём коллекцию для учебного списка задач. Она создаст запись, прочитает её и убедится, что ошибочный запрос не добавил лишнюю задачу.
Практикум Mentoring.digital рассчитан на начинающего тестировщика. Все данные учебные. Сервер запускается только на вашем компьютере, поэтому упражнение не требует чужого API, токена или доступа к рабочей базе.
| Запрос | Правило | Ожидается |
|---|---|---|
| GET /tasks | Получить все задачи | 200 и JSON-массив |
| POST /tasks | title: строка от 1 до 80 символов после trim | 201, id, title и done: false |
| GET /tasks/ID | Получить существующую задачу | 200 и данные этой задачи |
| POST /tasks с пустым title | Создание запрещено | 400, error: invalid_title, список не изменён |
| GET /tasks/999999 | Такой ID не создан в упражнении | 404, error: not_found |
Это выбранные правила нашего сервиса, а не обязательные коды любого API. Для настоящего проекта сначала согласуйте контракт. В частности, удаление пробелов, длина title и формат ошибки могут отличаться.
Нужны Python 3.10 или новее и настольный Postman. Сохраните следующий код в новой учебной папке как server.py, затем выполните python server.py. Терминал останется занят сервером. Для остановки используйте Ctrl+C.
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
items = {}
next_id = 1
class Handler(BaseHTTPRequestHandler):
def reply(self, status, data):
body = json.dumps(data).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/tasks":
return self.reply(200, list(items.values()))
if self.path.startswith("/tasks/"):
key = self.path.removeprefix("/tasks/")
if key.isdigit() and int(key) in items:
return self.reply(200, items[int(key)])
self.reply(404, {"error": "not_found"})
def do_POST(self):
global next_id
if self.path != "/tasks":
return self.reply(404, {"error": "not_found"})
try:
size = int(self.headers.get("Content-Length", "0"))
if not 0 < size <= 4096:
return self.reply(400, {"error": "invalid_body"})
data = json.loads(self.rfile.read(size))
except (ValueError, UnicodeError):
return self.reply(400, {"error": "invalid_json"})
title = data.get("title") if isinstance(data, dict) else None
if not isinstance(title, str) or not 1 <= len(title.strip()) <= 80:
return self.reply(400, {"error": "invalid_title"})
task = {"id": next_id, "title": title.strip(), "done": False}
items[next_id] = task
next_id += 1
self.reply(201, task)
if __name__ == "__main__":
HTTPServer(("127.0.0.1", 8765), Handler).serve_forever()
Это маленький однопоточный учебный сервер без авторизации и постоянного хранения. Не открывайте его в интернет. Он хранит задачи в памяти до остановки и ограничивает тело запроса 4096 байтами. После перезапуска список начинается заново. Для браузерной версии Postman локальный запрос может потребовать Desktop Agent; в упражнении проще использовать настольное приложение.
baseUrl со значением http://127.0.0.1:8765. Не задавайте одноимённую переменную в окружении: она может перекрыть значение коллекции.{{baseUrl}}/tasks. В Body выберите raw и JSON.{"title":"Check API"}. Проверьте заголовок Content-Type: application/json.pm.test("Создание: статус 201", () => {
pm.response.to.have.status(201);
});
const task = pm.response.json();
pm.test("Создание: данные задачи", () => {
pm.expect(task.id).to.be.a("number");
pm.expect(task.title).to.eql("Check API");
pm.expect(task.done).to.eql(false);
});
pm.collectionVariables.set("taskId", task.id);
Отдельно проверяется статус и содержимое. Сравнение done с false выявит, например, ошибочную строку "false". ID сохраняется после ответа и передаётся следующему запросу. Не подставляйте вручную 1: после повторного запуска коллекции сервер выдаст другой идентификатор.
Создайте второй запрос GET по адресу {{baseUrl}}/tasks/{{taskId}}. Для него добавьте:
pm.test("Чтение: статус 200", () => {
pm.response.to.have.status(200);
});
pm.test("Чтение: сохранённая задача", () => {
const task = pm.response.json();
pm.expect(task.id).to.eql(Number(pm.collectionVariables.get("taskId")));
pm.expect(task.title).to.eql("Check API");
pm.expect(task.done).to.eql(false);
});
Проверка чтения важна: правильный ответ POST не доказывает, что данные затем доступны. Если taskId не задан, сначала выполните создание. Если значение осталось от прошлого запуска, не объявляйте ошибку сервера до проверки переменной и порядка запросов.
Третьим запросом получите GET {{baseUrl}}/tasks и сохраните исходное число записей:
pm.test("Список до ошибки: статус 200", () => {
pm.response.to.have.status(200);
});
pm.collectionVariables.set("beforeCount", pm.response.json().length);
Четвёртый запрос: POST {{baseUrl}}/tasks с телом {"title":" "}. Проверка:
pm.test("Пустое название отклонено", () => {
pm.response.to.have.status(400);
pm.expect(pm.response.json().error).to.eql("invalid_title");
});
Пятый запрос снова получает список задач. Убедитесь, что он не вырос:
pm.test("Список после ошибки: статус 200", () => {
pm.response.to.have.status(200);
});
pm.test("Ошибочный запрос не создал задачу", () => {
pm.expect(pm.response.json().length)
.to.eql(Number(pm.collectionVariables.get("beforeCount")));
});
Если ограничиться статусом 400, можно пропустить дефект: сервис сначала создаёт запись, а затем возвращает ошибку. Сравнение состояния ловит именно этот нежелательный эффект. Условие сравнения корректно для изолированного учебного сервера; на общем стенде параллельные записи потребуют другого способа идентификации своих данных.
Сохраните запросы в порядке: создание, чтение, список до ошибки, пустое название, список после ошибки. Выберите Run у коллекции, функциональный локальный запуск и одну итерацию. Убедитесь, что включены все пять запросов. После выполнения откройте результат каждого теста, а не только общий цвет отчёта.
Повторный прогон должен работать без ручной подстановки ID. Он создаёт ещё одну корректную задачу, но ошибочный запрос по-прежнему не должен увеличивать список. Для чистого старта остановите и заново запустите учебный сервер. Это сбрасывает только его данные в памяти.
| Симптом | Что проверить |
|---|---|
| Connection refused | Сервер запущен, порт 8765 свободен и адрес совпадает |
| 404 у чтения | taskId получен в текущем запуске и URL собран правильно |
| Ошибка разбора JSON | Тело ответа действительно JSON, а не HTML другого сервиса |
| Тест не появился в отчёте | Скрипт сохранён в Post-response нужного запроса |
| Счётчик отличается | До и после ошибки проверяется один сервер без параллельных изменений |
Проверьте title длиной 1, 80 и 81 символ, число вместо строки, отсутствие поля, невалидный JSON и чтение отсутствующей записи. Для каждого случая заранее запишите ожидаемый ответ и изменение состояния. Набор проверок должен следовать контракту, а не состоять из случайных значений.
На курсах QA с практикой API можно разобрать ваш набор сценариев. Для индивидуальной обратной связи подойдёт ментор по тестированию: покажите коллекцию, контракт и объясните, какую ошибку обнаруживает каждая проверка.