Как стать автором
Обновить
688.07
Яндекс
Как мы делаем Яндекс

Пишем свой pastebin, используя только userver

Время на прочтение18 мин
Количество просмотров3.9K

Всем привет! Меня зовут Василий Куликов, я работаю ведущим разработчиком в Техплатформе Екома и Райдтеха Яндекса и последние пять лет разрабатываю фреймворк userver. Это веб‑фреймворк, который позволяет создавать высоконагруженные отказоустойчивые сервисы на С++. Сегодня я расскажу, как написать на нём игрушечный, но рабочий сервис, который реализует функциональность pastebin. Сервис будет выполнять роль бэкенда (и частично фронтенда) сайта, на котором можно запостить текстовую заметку и получить ссылку на неё. Также на этом примере можно увидеть, как пишется сервис на userver и какой API используется для решения повседневных задач бэкенда.

Общая архитектура

Сначала определимся с общей архитектурой сайта. Пусть это будет единственный stateless‑сервис upastebin со своей базой данных Postgres. Тот же upastebin будет отдавать статику (HTML, JS, CSS) из памяти. В более сложном варианте с тяжёлой статикой и динамическими страницами понадобился бы nginx с фронтенд‑фреймворком, но в нашем примере обойдёмся без них.

Определимся с тем, что должно быть реализовано в upastebin. Нам нужен эндпоинт, который будет отдавать страницу с конкретной заметкой. Чтобы не рендерить страницу на стороне бэкенда, мы будем рендерить её в браузере. За счёт этого она будет статичной и не будет отличаться от заметки к заметке.

Также нужна страница для создания новой заметки. Из API‑эндпоинтов нам нужны следующие: для создания заметки, для получения текста заметки и для получения списка последних заметок.

Для работы с базой данных (БД) нужны:

  • POST /api/v1/posts — для создания новой заметки;

  • GET /api/v1/posts/{id} — для получения текста конкретной заметки;

  • GET /api/v1/latest — для получения списка последних N заметок.

Для отдачи статики:

  • /{id} — для отдачи страницы с заметкой;

  • / — для написания нового поста;

  • /r/* — для отдачи остальной статики (JS, CSS).

Приготовления

Итак, приступаем к работе. Мы можем начать писать наш проект с нуля, а можем взять за основу готовый шаблон сервиса из списка. И нам вполне подойдёт один из шаблонов. Нажимаем кнопку Use this template и создаём новый проект. Согласно README, клонируем репозиторий:

git clone https://github.com/segoon/upastebin/ && cd upastebin

Для замены имени сервиса pg_service_template на upastebin копипастим из README команду переименования:

find . -not -path "./third_party/" -not -path ".git/" -not -path './build_*' -type f | xargs sed -i 's/pg_service_template/upastebin/g'

Теперь нам нужно подключить к проекту userver. Есть несколько способов, как это сделать:

  • скачать deb‑пакет с GitHub с последним релизом (сейчас есть пакеты для Ubuntu 22.04);

  • самостоятельно собрать userver и установить его либо через cmake ‑install, либо через cpack + dpkg ‑i;

  • подключить userver к нашему проекту как отдельную директорию и собирать его как часть проекта;

  • собрать проект в подготовленном докер‑образе Ubuntu 22.04 с предустановленным userver.

В нашем случае выберем путь отдельной директории:

cd third_party
git clone --depth 1 https://github.com/userver-framework/userver.git

Не забудьте добавить third_party/userver в gitignore! Если бы мы разрабатывали несколько сервисов, то более эффективным было бы клонировать userver в свою собственную директорию, а third_party/userver в каждом сервисе сделать симлинками на настоящий userver.

Теперь мы готовы запустить сборку сервиса командой:

make build-debug

Эта команда автоматически запустит cmake в директории build_debug и соберёт бинарник сервиса build_debug/upastebin. Для сборки релиза замените debug на release.

Прогнать тесты (юнит‑тесты gtest и testsuite, т. е. тесты уровня сервиса) можно с помощью команды:

make test-debug

Если хочется быстро запустить сервис локально, чтобы подёргать эндпоинты с помощью curl или открыть локальный сайт в браузере, можно воспользоваться следующей командой:

make service-start-debug

Она поднимет инстанс сервиса локально от текущего пользователя и временную базу Postgres. Сервис будет запущен со статическим конфигом, в котором указан порт временного Postgres‑демона. При завершении команды по Ctrl + C сервис, Postgres и остальное окружение будет завершено и подчищено.

Сейчас можем подёргать ручку /ping, которая проверяет живость сервиса с помощью curl:

$ curl -vs http://localhost:8080/ping
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /ping HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Wed, 22 Jan 2025 08:54:34 UTC
< Content-Type: application/octet-stream
< Server: userver/2.3-rc (20240801002050; rv:ac9b1cd)
< Accept-Encoding: gzip, zstd, identity
< X-YaRequestId: 5036899c44a844408d6a8c2520dc3556
< X-YaTraceId: da1003fad6ef4532a714350ff1c2a3a7
< X-YaSpanId: c48dc23f495a3476
< Connection: keep-alive
< Content-Length: 0
< 
* Connection #0 to host localhost left intact

Ожидаемо получаем статус 200, а также видим набор хедеров, добавляемых для отладки и трейсинга (X-Ya*).

Отдача статики

Наконец‑то начнём программировать! Добавим несколько эндпоинтов, для каждого из которых нужны секция с описанием в статическом конфиге и код этого эндпоинта (.cpp‑ и.hpp‑файлы).

Начнём с раздачи статики. Любой эндпоинт в userver описывается в виде класса, который унаследован от server::handlers::HttpHandlerBase. Инстанс класса будет создан при инициализации компонентной системы в процессе старта сервиса.

В конструкторе мы получаем ссылки на зависимости эндпоинта из других компонентов. Для раздачи статики нам нужен components::FsCache, а точнее его клиент fs::FsCacheClient, который на старте рекурсивно вычитывает содержимое директории в память и может очень быстро отдать его по запросу. При этом FsCacheClient регулярно перечитывает содержимое директории, что может оказаться полезным в случае изменений файлов без перезапуска сервиса (при желании можно отключить эту функцию).

В методе HandleRequestThrow() описывается код обработки HTTP‑запроса. Мы хотим взять параметр из URL, по нему сходить в FsCacheClient и получить содержимое скачиваемого файла. Для корректной работы в браузере нам также нужно выставить корректный Content‑Type, который мы будем определять по статическому отображению «расширение файла — тип контента». Если файл с таким именем не был найден, мы возвращаем код ошибки 404. При желании код ошибки можно сопроводить телом для отображения в браузере, но мы это опустим для простоты.

Пишем хедер:

src/resources.hpp
#pragma once

#include <userver/http/common_headers.hpp>
#include <userver/server/handlers/http_handler_base.hpp>
#include <userver/fs/fs_cache_client.hpp>


namespace upastebin {

class ResourcesHandler final
    : public userver::server::handlers::HttpHandlerBase {
 public:
  // Имя требуется для регистрации компонента, для поиска настроек в статическом конфиге и для поиска со стороны других компонентов
  static constexpr std::string_view kName = "handler-resources";

  ResourcesHandler(const userver::components::ComponentConfig& config,
                   const userver::components::ComponentContext& context);

  std::string HandleRequestThrow(
      const userver::server::http::HttpRequest& request,
      userver::server::request::RequestContext&) const override;

 private:
  const userver::fs::FsCacheClient& fs_client_;
};

}  // namespace upastebin

Пишем cpp‑файл:

src/resources.cpp
#include "resources.hpp"

#include <userver/components/component_config.hpp>
#include <userver/components/component_context.hpp>
#include <userver/components/fs_cache.hpp>

namespace upastebin {

namespace {

std::string_view GetContentType(std::string_view extention) {
  if (extention == ".js")
    return "application/javascript";
  else if (extention == ".css")
    return "text/css";
  else if (extention == ".html")
    return "text/html; charset=UTF-8";
  else
    return "application/octet-stream";
}

}  // namespace

ResourcesHandler::ResourcesHandler(
    const userver::components::ComponentConfig& config,
    const userver::components::ComponentContext& context)
    : HttpHandlerBase(config, context),
      fs_client_(
          context.FindComponent<userver::components::FsCache>("resources-cache")
              .GetClient()) {}

std::string ResourcesHandler::HandleRequestThrow(
    const userver::server::http::HttpRequest& request,
    userver::server::request::RequestContext&) const {
  auto subpath = request.GetPathArg("subpath");
  // Note: нет нужды валидировать subpath, т.к. FsCacheClient не отдаст файл на отсутствующий путь

  auto file_ptr = fs_client_.TryGetFile("/" + subpath);
  auto& response = request.GetHttpResponse();
  if (file_ptr) {
    response.SetContentType(GetContentType(file_ptr->extension));
    return file_ptr->data;
  } else {
    auto& response = request.GetHttpResponse();
    response.SetStatus(userver::server::http::HttpStatus::kNotFound);
    return {};
  }
}

}  // namespace upastebin

Конструктор обычно содержит в себе две вещи: вычитывание из ComponentConfig тех параметров, которые указаны в секции компонента в статическом конфиге, и поиск других компонентов через интерфейс ComponentContext. В нашем случае параметров у компонента эндпоинта нет, поэтому мы только ищем компонент FsCache по типу, из него достаём клиента и сохраняем по ссылке в поле класса. После выхода из конструктора компонент готов к работе.

В HandleRequestThrow мы должны сделать всё, чтобы обработать входящий HTTP‑запрос. Каждый запрос обрабатывается в независимой корутине. В процессе обработки запросов может создаваться множество корутин, много больше, чем число потоков ОС. Если в эндпоинте мы ходим в базу или в другие сервисы, то из‑за ожидания корутина может долгое время находиться в состоянии сна. Это состояние не отнимает процессорного времени, но занимает некоторое количество памяти. Если есть риски превышения доступной памяти, можно ограничить число одновременно обрабатываемых запросов (параметр max_requests_in_flight) или RPS (параметр max_requests_per_second). Подробнее это описано в документации userver.

Далее мы получаем параметр пути из request.GetPathArg(). Query‑параметр мы получали бы через GetArg(). Указатель на файл в памяти получаем через TryGetFile(), который возвращает shared_ptr. Если указатель пустой, то мы получаем HttpResponse из HttpRequest через GetHttpResponse() и выставляем статус 404 через SetStatus(). Если файл найден, мы смотрим на его расширение, исходя из чего выставляем хедер Content‑Type с помощью вызова HttpResponse::SetContentType(). На этом код эндпоинта готов.

Теперь займёмся конфигом. У каждого компонента есть своя подсекция конфига в yaml‑файле в секции components_manager.components.<component>. Для эндпоинта там описываются обрабатываемый путь, принимаемые методы, используемый таск‑процессор и другие настройки.

Для большинства ручек подойдёт основной таск‑процессор main-task-processor, в котором запускаются все корутины по умолчанию. В других таск‑процессорах могут работать корутины, которые способны заблокировать поток ОС (обычно fs-task-processor), а также специальные корутины, которые должны работать даже при частичном отказе основных таск‑процессоров (обычно stats-task-processor для отдачи метрик).

configs/static_config.yaml
            handler-resources:
                path: /r/{subpath}
                method: GET
                task_processor: main-task-processor

Наш эндпоинт готов. Зарегистрируем его в компонентной системе, добавив в main():

Если мы сейчас попробуем запустить сервис, получим ошибку отсутствия компонента resources-cache. Мы его запросили, но не зарегистрировали в компонентной системе и в статическом конфиге. Исправим ситуацию и добавим компонент в main.cpp:

component_list.Append<userver::components::FsCache>("resources-cache");

И в статический конфиг:

        resources-cache:
            dir: /var/www
            update-period: 1m
            fs-task-processor: fs-task-processor

Теперь сервис стартует.

Отдача / и /{id} будет упрощённой версией этого компонента. Вместо выбора файла будет отдаваться статичная страница из кеша.

Сохранение заметки

Напишем эндпоинт для сохранения новой заметки. Это будут файлы src/store.{hpp,cpp}. Заведём новый класс StoreHandler, унаследованный от server::handlers::HttpHandlerBase. Добавим в него поле типа storages::postgres::ClusterPtr, потому что мы хотим обращаться к Postgres.

Пишем hpp‑файл:

src/store.hpp
#pragma once

#include <userver/server/handlers/http_handler_base.hpp>
#include <userver/storages/postgres/postgres_fwd.hpp>

namespace upastebin {

class StoreHandler final : public userver::server::handlers::HttpHandlerBase {
 public:
  static constexpr std::string_view kName = "handler-store";

  StoreHandler(const userver::components::ComponentConfig& config,
               const userver::components::ComponentContext& context);

  std::string HandleRequestThrow(
      const userver::server::http::HttpRequest& request,
      userver::server::request::RequestContext&) const override;

 private:
  userver::storages::postgres::ClusterPtr pg_;
};

}  // namespace upastebin

Заметим, что используется инклуд‑файл postgres_fwd.hpp, а не postgres.hpp. Для многих хедеров существует облегчённый вариант с _fwd, который только объявляет типы, но не описывает их. Это позволяет заинклудить гораздо более лёгкий хедер из большого числа пользовательских хедеров. Это может существенно ускорить компиляцию большого проекта.

В конструкторе будем искать компонент userver::components::Postgres и забирать у него указатель на кластер через GetCluster(). Через этот указатель мы сможем обращаться к БД.

В HandleRequestThrow() мы должны сгенерировать новый id для заметки — для простоты пусть это будет UUID. Из query забираем параметр author, из тела забираем текст заметки, из HttpRequest забираем IP пользователя. После этого делаем запрос к БД на INSERT. В тексте ответа возвращаем JSON с id заметки.

Пишем cpp‑файл:

src/store.cpp
#include "store.hpp"

#include <userver/components/component_context.hpp>
#include <userver/formats/json/inline.hpp>
#include <userver/http/common_headers.hpp>
#include <userver/server/handlers/exceptions.hpp>
#include <userver/server/handlers/http_handler_base.hpp>
#include <userver/storages/postgres/cluster.hpp>
#include <userver/storages/postgres/component.hpp>
#include <userver/utils/boost_uuid4.hpp>
#include <userver/utils/datetime.hpp>

namespace upastebin {

StoreHandler::StoreHandler(const userver::components::ComponentConfig& config,
                           const userver::components::ComponentContext& context)
    : HttpHandlerBase(config, context),
      pg_(context.FindComponent<userver::components::Postgres>("postgres")
              .GetCluster()) {}

std::string StoreHandler::HandleRequestThrow(
    const userver::server::http::HttpRequest& request,
    userver::server::request::RequestContext&) const {
  auto uuid = userver::utils::generators::GenerateBoostUuid();
  auto author = request.GetArg("author");
  auto ip_source = request.GetRemoteAddress().PrimaryAddressString();
  auto text = request.RequestBody();
  auto created_at = userver::utils::datetime::Now();

  pg_->Execute(userver::storages::postgres::ClusterHostType::kMaster,
               "INSERT INTO upastebin.texts (uuid, author, ip_source, text, created_at) VALUES "
               "($1, $2, $3, $4, $5);",
               uuid, author, ip_source, text, created_at);

  auto json_response = userver::formats::json::MakeObject("uuid",
                                            userver::utils::ToString(uuid));
  return ToString(json_response);
}

}  // namespace upastebin

Для вычисления текущего времени мы используем utils::datetime::Now() вместо std::chrono::system_clock::now(), так как первый вариант обладает возможностью мокать время в тестах. Это может пригодиться при написании тестов и сверке результатов INSERT. Сам запрос отправляется через Cluster::Execute().

Cluster — абстрактный интерфейс к БД. За ним стоит не одно соединение, а целый пул. Драйвер Postgres самостоятельно поддерживает его в актуальном состоянии, переоткрывая соединения при их недостатке, согласно настройкам статического конфига. В крайнем случае Execute() синхронно откроет соединение с БД.

Первый аргумент Execute() — тип хоста в кластере БД. Может быть мастер, синк слейв, обычный слейв, ближайший хост. Для записи в БД нам нужен мастер.

Второй аргумент у Execute() — сам SQL‑запрос. Следом идут параметры этого запроса. Если он будет выполнен, то Execute() вернёт ResultSet с результатами. В нашем случае они неинтересны, поэтому результат игнорируется. Если SQL‑запрос завершится по ошибке любого типа — сетевой, серверной (например, конфликт), клиентской (например, синтаксическая ошибка) — Execute() выкинет исключение. Это исключение мы никак не обрабатываем: код, вызывающий HandleRequestThrow(), по умолчанию ловит все исключения и выставляет код HTTP‑ошибки 500, что для нас вполне подходит. В конце обработки мы создаём JSON‑объект с единственным полем id заметки и возвращаем его из эндпоинта в теле ответа.

Получение заметки

Перейдём к эндпоинту получения заметки по id. Заголовочный файл отличается от от прошлого файла src/store.hpp только именем класса и значением kName. В HandleRequestThrow() нам нужно взять id из query, сконвертить его в UUID и сделать запрос к БД. Execute() возвращает ResultSet, из которого можно получить единственный элемент и его поле с именем text. Возвращаем это поле в виде тела ответа. Если ResultSet оказался пустым, то заметка с таким id не найдена и мы возвращаем страницу 404.

src/retrieve_by_uuid.cpp
#include "retrieve_by_uuid.hpp"

#include <userver/components/component_context.hpp>
#include <userver/storages/postgres/cluster.hpp>
#include <userver/storages/postgres/component.hpp>
#include <userver/utils/boost_uuid4.hpp>

namespace upastebin {

RetrieveHandler::RetrieveHandler(
    const userver::components::ComponentConfig& config,
    const userver::components::ComponentContext& context)
    : HttpHandlerBase(config, context),
      pg_(context.FindComponent<userver::components::Postgres>("postgres")
              .GetCluster()) {}

std::string RetrieveHandler::HandleRequestThrow(
    const userver::server::http::HttpRequest& request,
    userver::server::request::RequestContext&) const {
  auto uuid_str = request.GetPathArg("uuid");
  auto uuid = userver::utils::BoostUuidFromString(uuid_str);

  auto result = pg_->Execute(
      userver::storages::postgres::ClusterHostType::kSlave,
      "SELECT text FROM upastebin.texts WHERE uuid=($1::TEXT);", uuid);
  if (result.Size() == 0) {
    request.GetHttpResponse().SetStatusNotFound();
    return {};
  }

  auto row = result.Front();
  auto text = row["text"].As<std::string>();
  return text;
}

}  // namespace upastebin

Список последних заметок

Нам нужна ещё одна ручка для получения набора последних заметок, чтобы вывести их списком на странице. Создаём ещё один эндпоинт — LatestHandler. Хедер устроен точно так же, как и у предыдущих двух. В HandleRequestThrow() мы должны сделать SELECT‑запрос к Postgres для получения последних N заметок. Execute() возвращает нам эти заметки в ResultSet. Мы можем по нему итерироваться с помощью for-each.

Сами заметки мы будем отдавать в JSON‑форме. Работа с JSON происходит одним из двух способов. Во‑первых, для доступа read‑only используется formats::json::Value — лёгкая обёртка над нодой дерева JSON. Во‑вторых, для создания нового JSON value используется билдер formats::json::ValueBuilder. Разделение требуется для упрощения краевых случаев, в которых происходит присваивание типа x["foo"] = y["bar"]. Тут может возникнуть путаница: это рекурсивное копирование дерева или копирование ссылки на поддерево? Чтобы не возникало подобных вопросов, типы на чтение и запись разнесены. Результат сборки JSON получает через ExtractValue() у билдера.

Наша ручка возвращает JSON, из‑за чего её можно было бы отнаследовать от JsonHandlerBase. В этом случае можно было бы не совершать лишние операции по сериализации JSON в строку. Однако JSON handler и принимает JSON‑запрос, и возвращает JSON‑ответ, а мы лишь возвращаем JSON.

Код src/latest.cpp:

src/latest.cpp
#include "latest.hpp"

#include <userver/components/component_context.hpp>
#include <userver/formats/json/inline.hpp>
#include <userver/formats/json/value_builder.hpp>
#include <userver/storages/postgres/cluster.hpp>
#include <userver/storages/postgres/component.hpp>
#include <userver/utils/boost_uuid4.hpp>

namespace upastebin {

LatestHandler::LatestHandler(
    const userver::components::ComponentConfig& config,
    const userver::components::ComponentContext& context)
    : HttpHandlerBase(config, context),
      pg_(context.FindComponent<userver::components::Postgres>("postgres")
              .GetCluster()) {}

std::string LatestHandler::HandleRequestThrow(
    const userver::server::http::HttpRequest&,
    userver::server::request::RequestContext&) const {
  auto result = pg_->Execute(
      userver::storages::postgres::ClusterHostType::kSlave,
      "SELECT author, substring(text for $1::INTEGER) AS text_tr, ip_source "
      "FROM "
      "upastebin.texts ORDER BY created_at DESC LIMIT $2::INTEGER;",
      1000, 10);

  userver::formats::json::ValueBuilder response(
      userver::formats::common::Type::kArray);
  for (const auto& item : result) {
    userver::formats::json::ValueBuilder response_item;

    response_item["author"] = item["author"].As<std::string>();
    response_item["ip"] = item["ip_source"].As<std::string>();
    response_item["text"] = item["text_tr"].As<std::string>();

    response.PushBack(response_item.ExtractValue());
  }
  return ToString(
      userver::formats::json::MakeObject("items", response.ExtractValue()));
}

}  // namespace upastebin

Тесты

Мы написали самые простые эндпоинты для создания и просмотра заметок — давайте теперь напишем на них тесты!

Для userver тесты пишутся на фреймворке yandex‑taxi‑testsuite. Он основан на pytest и предоставляет большой набор фикстур и инструментов для работы с сервисами, моками, базами данных и т. д. Для работы тестов поднимается upastebin в замоканном окружении, в котором поднят свой PostgreSQL.

Чтобы сервис в testsuite‑окружении корректно работал, ему необходима БД с инициализированной схемой данных. Эта схема описывается в файлах postgresql/schemas/*.sql. Ожидается, что одни и те же файлы используются для инициализации схемы данных и в тестах, и в продакшне. Поэтому схема данных записывается в форме файлов миграций, которые никогда не меняются, но изменения дописываются в новые файлы миграций.

Мы обойдёмся одним файлом миграций db_1.sql:

postgresql/schemas/db_1.sql
DROP SCHEMA IF EXISTS upastebin CASCADE;

CREATE SCHEMA IF NOT EXISTS upastebin;

CREATE TABLE IF NOT EXISTS upastebin.texts (
    uuid TEXT PRIMARY KEY,
    author TEXT NOT NULL,
    ip_source TEXT NOT NULL,
    text TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL
);

Чтобы testsuite работал с локальной директорией со статическими файлами, а не с /var/www-data, ему необходимо подменить путь к директории в статическом конфиге. Для этого мы должны в файле conftest.py добавить соответствующую фикстуру и прописать её в список USERVER_CONFIG_HOOKS. В этом списке хранятся имена фикстур, которые меняют статический конфиг. В самой фикстуре мы возвращаем функцию, которая меняет аргумент‑конфиг.

Первым аргументом функции выступает статический конфиг, представленный в форме словаря. В нём мы должны поменять все значения, которые специфичны для testsuite.

tests/conftest.py
USERVER_CONFIG_HOOKS = ['prepare_service_config_resources']

WWW_DATA = pathlib.Path(__file__).parent.parent / 'www-data'
...
@pytest.fixture(scope='session')
def prepare_service_config_resources():
    def patch_config(config, config_vars):
        components = config['components_manager']['components']
        components['resources-cache']['dir'] = str(WWW_DATA)

    return patch_config

Теперь, если мы запустим команду из корня репозитория make test-debug, она отработает без ошибок.

Добавим туда тестов — разберём один из них по косточкам. Добавим исходник pytest‑теста в директорию tests/ с именем с префиксом test_:

tests/test_crud.py
...
async def test_create_and_retrieve(service_client):
    response = await service_client.post(
        '/api/v1/posts/', params={'author': 'foo'}, data=TEXT,
    )
    assert response.status == 200

    json_response = response.json()
    assert list(json_response) == ['uuid']
    uuid = json_response['uuid']

    response = await service_client.get(f'/api/v1/posts/{uuid}')
    assert response.status == 200
    assert response.text == TEXT

    response = await service_client.get('/api/v1/latest')
    assert response.status == 200
    assert response.json() == {
        'items': [{'author': 'foo', 'ip': '::ffff:127.0.0.1', 'text': TEXT}],
    }

По сути, тест — это функция, которая в pytest‑тесте должна начинаться с test_ и принимать аргументы‑фикстуры. В нашем случае тесту требуется фикстура service_client, с помощью которой происходит обращение к сервису upastebin. Мы хотим написать тест, который постит заметку, получает её по айдишнику, а также находит её в списке последних заметок.

GET/POST‑запрос происходит с помощью метода get/post фикстуры service_client. Передаём туда HTTP path, query parameters в params, тело запроса в data. В результате получаем response, у которого поле status равняется HTTP status code, метод json возвращает словарь или список с распаршенным JSON‑телом ответа, а поле text содержит plaintext тела ответа.

Проверяем, что ручки сервиса отвечают успешно и возвращают ожидаемые данные.

Если мы теперь запустим команду make test-debug, то увидим успешно пройденные тесты:

make test-debug

1: collecting... collected 9 items1:1:../tests/test_crud.py::test_404 PASSED [ 11%]
1:../tests/test_crud.py::test_empty_db PASSED [ 22%]
1:../tests/test_crud.py::test_create_and_retrieve PASSED [ 33%]
1:../tests/test_resources.py::test_found[index.html‑text/html; charset=UTF-8] PASSED [ 44%]
1:../tests/test_resources.py::test_found[index.js‑application/javascript] PASSED [ 55%]
1:../tests/test_resources.py::test_not_found[] PASSED [ 66%]
1:../tests/test_resources.py::test_not_found[.] PASSED [ 77%]
1:../tests/test_resources.py::test_not_found[..] PASSED [ 88%]
1:../tests/test_resources.py::test_not_found[missing.html] PASSED [100%]
1:
1: ‑--‑--‑--‑--‑--‑--‑--‑--‑ Service logs ‑--‑--‑--‑--‑--‑--‑--‑--‑
1: — service log file: /home/segoon/projects/upastebin/build_debug/Testing/Temporary/service.log —
1: ============================== 9 passed in 1.68s ===============================
1/1 Test #1: testsuite‑upastebin.............. Passed 1.99 sec

Если вам не хватило информации о фикстурах, то загляните в документацию. Чтобы изучить все существующие фикстуры, вы можете зайти в директорию third_party/userver/testsuite/ для поиска userver‑специфичных фикстур или в build_debug/venv-userver-default/lib/python3.10/site-packages/testsuite/ для поиска базовых фикстур (testsuite по умолчанию скачивается в билд‑директорию). Фикстуры помечены декоратором @pytest.fixture.


Итак, мы рассмотрели процесс написания простого сервиса на userver. Конечно, с помощью этого фреймворка можно писать гораздо более сложные сервисы, составленные из тысяч и десятков тысяч строк. Если он вам приглянулся и вы хотите продолжить с ним работать, загляните на страницу userver и в телеграм‑чат (есть и отдельный чат на английском).

Исходники примера можно скачать с GitHub.

Теги:
Хабы:
+38
Комментарии10

Полезные ссылки

Грязные трюки C++ из userver и Boost

Уровень сложностиСредний
Время на прочтение15 мин
Количество просмотров17K
Всего голосов 54: ↑54 и ↓0+71
Комментарии67

userver 2.0 — большой релиз фреймворка для IO-bound программ

Время на прочтение6 мин
Количество просмотров9.6K
Всего голосов 43: ↑43 и ↓0+57
Комментарии18

Информация

Сайт
www.ya.ru
Дата регистрации
Дата основания
Численность
свыше 10 000 человек
Местоположение
Россия