Тестирование API в Postman: от первого запроса до проверяемой коллекции

Тестирование API в Postman: от первого запроса до проверяемой коллекции
Содержание статьи

Тестирование API в Postman начинается с понятного контракта: что отправляем, какой ответ ожидаем и как проверяем сохранённое состояние. Соберём коллекцию для учебного списка задач. Она создаст запись, прочитает её и убедится, что ошибочный запрос не добавил лишнюю задачу.

Практикум Mentoring.digital рассчитан на начинающего тестировщика. Все данные учебные. Сервер запускается только на вашем компьютере, поэтому упражнение не требует чужого API, токена или доступа к рабочей базе.

Контракт учебного сервиса

ЗапросПравилоОжидается
GET /tasksПолучить все задачи200 и JSON-массив
POST /taskstitle: строка от 1 до 80 символов после trim201, 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 и формат ошибки могут отличаться.

Запустите локальный API

Нужны 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; в упражнении проще использовать настольное приложение.

Создайте коллекцию и первый POST-запрос

  1. Создайте коллекцию «Учебные задачи: API».
  2. Добавьте переменную коллекции baseUrl со значением http://127.0.0.1:8765. Не задавайте одноимённую переменную в окружении: она может перекрыть значение коллекции.
  3. Добавьте запрос POST по адресу {{baseUrl}}/tasks. В Body выберите raw и JSON.
  4. Укажите тело {"title":"Check API"}. Проверьте заголовок Content-Type: application/json.
  5. В Scripts → Post-response добавьте проверки ниже и нажмите Send.
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 можно разобрать ваш набор сценариев. Для индивидуальной обратной связи подойдёт ментор по тестированию: покажите коллекцию, контракт и объясните, какую ошибку обнаруживает каждая проверка.

Похожие статьи по данной теме