Как стать автором
Поиск
Написать публикацию
Обновить
799.96
Яндекс
Как мы делаем Яндекс

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

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

Всем привет! Меня зовут Василий Куликов, я работаю ведущим разработчиком в Техплатформе Екома и Райдтеха Яндекса и последние пять лет разрабатываю фреймворк 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.

Теги:
Хабы:
Всего голосов 31: ↑30 и ↓1+38
Комментарии10

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

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

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

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

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

Информация

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