Яндекс открывает фреймворк Testsuite

от автора

Сегодня мы открываем исходный код testsuite — фреймворка для тестирования HTTP-сервисов, который разработан и применяется в Яндекс.Такси. Исходники опубликованы на GitHub под лицензией MIT.

С помощью testsuite удобно тестировать HTTP-сервисы. Он предоставляет готовые механизмы, чтобы:

— Взаимодействовать с сервисом через вызовы его HTTP API.
— Перехватить и обработать HTTP-вызовы, которые сервис отправляет во внешние сервисы.
— Проверить, какие вызовы во внешние сервисы сделаны и в каком порядке.
— Взаимодействовать с базой данных сервиса, чтобы создать предусловие или проверить результат.

Область применения

Бэкенд Яндекс.Такси состоит из сотен микросервисов, постоянно появляются новые. Все высоконагруженные сервисы мы разрабатываем на С++ с использованием собственного фреймворка userver, о нём мы уже рассказывали на Хабре. Менее требовательные к нагрузке сервисы, а также прототипы делаем на Python.

Чтобы убедиться, что сервис хорошо решает свою задачу, предоставляя API другим сервисам и конечному приложению, мы хотим тестировать его как целое, преимущественно по принципу чёрного ящика.

Готовых инструментов для этого нет — вам пришлось бы писать код для настройки тестового окружения, который будет:

— поднимать и наливать базу данных;
— перехватывать и подменять HTTP-запросы;
— запускать в этом окружении тестируемый сервис.

Решать эту задачу, пользуясь фреймворками для unit-тестов, слишком трудно и неправильно, потому что их задача другая: модульное тестирование более мелких структурных единиц — компонентов, классов, функций.

В основе testsuite лежит pytest, стандартный для Python тестовый фреймворк. При этом неважно, на каком языке написан микросервис, который мы тестируем. Сейчас testsuite работает на операционных системах GNU/Linux, macOS.

Хотя testsuite удобен для интеграционных сценариев, то есть взаимодействия нескольких сервисов (а если сервис написан на Python — то и для низкоуровневых), эти случаи мы рассматривать не будем. Далее речь пойдёт только о тестировании отдельно взятого сервиса.

Уровень детализации Инструмент тестирования
Метод/функция, класс, компонент, библиотека Стандартные unit-тесты, pytest, Googletest, иногда всё-таки testsuite
Микросервис testsuite
Ансамбль микросервисов (приложение) Интеграционные тесты testsuite (в этой статье не рассматриваются)

Принцип действия

Конечная цель — убедиться, что сервис правильно отвечает на HTTP-вызовы, поэтому тестируем через HTTP-вызовы.

Запуск/остановка сервиса — это рутинная операция. Поэтому проверяем:

— что после запуска сервис отвечает по HTTP;
— как ведёт себя сервис, если внешние сервисы временно недоступны.


Testsuite:

— Запускает базу данных (PostgreSQL, MongoDB…).
— Перед каждым тестом наполняет базу тестовыми данными.
— Запускает тестируемый микросервис в отдельном процессе.
— Запускает собственный веб-сервер (mockserver), который имитирует (мокает) для сервиса внешнее окружение.
— Выполняет тесты.

Тесты могут проверять:

— Правильно ли сервис обрабатывает HTTP-запросы.
— Как работает сервис непосредственно в базе данных.
— Наличие/отсутствие/последовательность вызовов во внешние сервисы.
— Внутреннее состояние сервиса с помощью информации, который тот передаёт в Testpoint.

mockserver

Мы тестируем поведение отдельного микросервиса. Вызовы HTTP API внешних сервисов должны быть замоканы. За эту часть работы в testsuite отвечают его собственные плагины mockserver и mockserver_https. Mockserver — это HTTP-сервер с настраиваемыми на каждый тест обработчиками запросов и памятью о том, какие запросы обработаны и какие при этом переданы данные.

База данных

Testsuite позволяет тесту напрямую обращаться к базе данных для чтения и записи. С помощью данных можно формировать предусловие теста и проверять результат. Из коробки поддержаны PostgreSQL, MongoDB, Redis.

Как начать пользоваться

Чтобы писать тесты testsuite, разработчик должен знать Python и стандартный фреймворк pytest.

Продемонстрируем использование testsuite пошагово на примере простого чата. Здесь исходные коды приложения и тестов.

Фронтенд chat.html взаимодействует с сервисом chat-backend.

Чтобы продемонстрировать взаимодействие сервисов, chat-backend делегирует хранение сообщений сервису хранилища. Хранилище реализовано двумя способами, chat-storage-mongo и chat-storage-postgres.

chat-backend

Сервис chat-backend — точка входа для запросов с фронтенда. Умеет отправлять и возвращать список сообщений.

Сервис

Покажем для примера обработчик запроса POST /messages/retrieve:

Исходный код

@routes.post('/messages/retrieve') async def handle_list(request): async with aiohttp.ClientSession() as session:     # Получить сообщения из сервиса хранилища     response = await session.post(         storage_service_url + 'messages/retrieve',             timeout=HTTP_TIMEOUT,         )         response.raise_for_status()         response_body = await response.json()          # Обратить порядок полученных сообщений, чтобы последние были в конце списка         messages = list(reversed(response_body['messages']))         result = {'messages': messages}         return web.json_response(result)

Тесты

Подготовим инфраструктуру testsuite к запуску сервиса. Укажем, с какими настройками мы хотим запускать сервис.

Исходный код

# Запускаем сервис один раз на сессию.  # Можно запускать и на каждый тест (убрать scope='session'), но это медленно @pytest.fixture(scope='session') async def service_daemon(         register_daemon_scope, service_spawner, mockserver_info, ):     python_path = os.getenv('PYTHON3', 'python3')     service_path = pathlib.Path(__file__).parent.parent     async with register_daemon_scope(             name='chat-backend',             spawn=service_spawner(                 # Команда запуска сервиса. Первый элемент массива — исполняемый файл,                 # далее аргументы командной строки                 [                     python_path,                     str(service_path.joinpath('server.py')),                     '--storage-service-url',                     # Направим запросы в сервис хранилища в mockserver,                     # далее в тестах мы настроим обработку запросов в mockserver по пути /storage                     mockserver_info.base_url + 'storage/',                 ],                 # Диагностический URL, отвечает на запросы после успешного запуска                 check_url=SERVICE_BASEURL + 'ping',             ),     ) as scope:         yield scope

Зададим фикстуру клиента, через неё тест отправляет HTTP-запрос в сервис.

Исходный код

@pytest.fixture async def server_client(         service_daemon, # HTTP-статус ответа == 204         service_client_options,         ensure_daemon_started,         # Зависимость от mockserver нужна, чтобы любой тест завершился с ошибкой,         # если сервис отправил запрос, который мы забыли замокать         mockserver, ):     await ensure_daemon_started(service_daemon)     yield service_client.Client(SERVICE_BASEURL, **service_client_options)

Теперь инфраструктура знает, как запустить chat-backend и как отправить в него запрос. Этого достаточно, чтобы приступить к написанию тестов.

Обратите внимание, в тестах chat-backend мы никак не используем сервисы хранилища, ни chat-storage-mongo, ни chat-storage-postgres. Чтобы chat-backend нормально обработал вызовы, мы мокаем API хранилища с помощью mockserver.

Напишем тест на метод POST messages/send. Проверим, что:
— запрос обработается штатно;
— при обработке запроса chat-backend вызывает метод хранилища POST messages/send.

Исходный код

async def test_messages_send(server_client, mockserver):     # Замокаем с помощью mockserver метод хранилища POST messages/send     @mockserver.handler('/storage/messages/send')         async def handle_send(request):         # Убедимся, что в хранилище отправлено то самое сообщение,         # которое мы отправляем в chat-backend         assert request.json == {             'username': 'Bob',             'text': 'Hello, my name is Bob!',         }         return mockserver.make_response(status=204)      # Отправим запрос в chat-backend     response = await server_client.post(         'messages/send',         json={'username': 'Bob', 'text': 'Hello, my name is Bob!'},     )          # Проверим, что запрос обработан штатно и вернул ожидаемый HTTP-статус     assert response.status == 204      # Проверим, что chat-backend один раз отправил в хранилище запрос POST messages/send     assert handle_send.times_called == 1

Напишем тест на метод POST messages/retrieve. Проверим, что:
— запрос обработан штатно;
— при обработке запроса chat-backend вызывает метод хранилища POST /messages/retrieve;
chat-backend «переворачивает» список сообщений, полученный из хранилища, чтобы последние сообщения были в конце списка.

Исходный код

async def test_messages_retrieve(server_client, mockserver):     messages = [         {             'username': 'Bob',             'created': '2020-01-01T12:01:00.000',             'text': 'Hi, my name is Bob!',         },         {             'username': 'Alice',             'created': {'$date': '2020-01-01T12:02:00.000'},             'text': 'Hi Bob!',         },     ]      # Замокаем с помощью mockserver метод хранилища POST messages/retrieve     @mockserver.json_handler('/storage/messages/retrieve')     async def handle_retrieve(request):         return {'messages': messages}      # Отправим запрос в chat-backend     response = await server_client.post('messages/retrieve')      # Проверим, что запрос обработан штатно и вернул ожидаемый HTTP-статус     assert response.status == 200      body = response.json()          # Проверим, что в ответе chat-backend порядок сообщений обратен порядку,     # который отдаёт хранилище, чтобы последние сообщения оказались в конце списка     assert body == {'messages': list(reversed(messages))}      # Проверим, что chat-backend один раз отправил в хранилище запрос POST messages/retrieve     assert handle_retrieve.times_called == 1

chat-storage-postgres

Сервис chat-storage-postgres отвечает за чтение и запись сообщений чата в базу данных PostgreSQL.

Сервис

Вот так мы читаем список сообщений из PostgreSQL в методе POST /messages/retrieve:

Исходный код

@routes.post('/messages/retrieve')     async def get(request):         async with app['pool'].acquire() as connection:             records = await connection.fetch(                 'SELECT created, username, "text" FROM messages '                 'ORDER BY created DESC LIMIT 20',             )         messages = [             {                 'created': record[0].isoformat(),                 'username': record[1],                 'text': record[2],             }             for record in records         ]         return web.json_response({'messages': messages}) 

Тесты

Сервис, который мы тестируем, использует базу данных PostgreSQL. Чтобы всё работало, нам достаточно указать testsuite, в какой директории искать схемы таблиц.

Исходный код

@pytest.fixture(scope='session') def pgsql_local(pgsql_local_create):     # Укажем, в какой директории искать схемы     tests_dir = pathlib.Path(__file__).parent     sqldata_path = tests_dir.joinpath('../schemas/postgresql')     databases = discover.find_databases('chat_storage_postgres', sqldata_path)     return pgsql_local_create(list(databases.values()))

В остальном настройка инфраструктуры conftest.py не отличается от описанного выше сервиса chat-backend.

Перейдём к тестам.

Напишем тест на метод POST messages/send. Проверим, что он сохраняет сообщение в базу данных.

Исходный код

async def test_messages_send(server_client, pgsql):     # Отправим запрос POST /messages/send     response = await server_client.post(         '/messages/send', json={'username': 'foo', 'text': 'bar'},     )      # Проверим, что запрос обработан штатно     assert response.status_code == 200      # Проверим, что в теле ответа JSON с идентификатором сохранённого сообщения     data = response.json()     assert 'id' in data      # Найдём сохранённое сообщение в PostgreSQL по идентификатору     cursor = pgsql['chat_messages'].cursor()     cursor.execute(         'SELECT username, text FROM messages WHERE id = %s', (data['id'],),     )     record = cursor.fetchone()      # Проверим, что в сохранённом сообщении те же имя пользователя и текст,      # что были отправлены в HTTP-запросе     assert record == ('foo', 'bar')

Напишем тест на метод POST messages/retrieve. Проверим, что он возвращает сообщения из базы данных.

Для начала создадим скрипт, который добавит в таблицу нужные нам записи. Testsuite автоматически выполнит скрипт перед тестом.

Исходный код

-- файл chat-storage-postgres/tests/static/test_service/pg_chat_messages.sql INSERT INTO messages(id, created, username, text) VALUES (DEFAULT, '2020-01-01 00:00:00.0+03', 'foo', 'hello, world!'); INSERT INTO messages(id, created, username, text) VALUES (DEFAULT, '2020-01-01 00:00:01.0+03', 'bar', 'happy ny');

Исходный код

# файл chat-storage-postgres/tests/test_service.py async def test_messages_retrieve(server_client, pgsql):     # Перед выполнением этого теста testsuite запишет в базу данные из     # скрипта pg_chat_messages.sql     response = await server_client.post('/messages/retrieve', json={})     assert response.json() == {         'messages': [             {                 'created': '2019-12-31T21:00:01+00:00',                 'text': 'happy ny',                 'username': 'bar',             },             {                 'created': '2019-12-31T21:00:00+00:00',                 'text': 'hello, world!',                 'username': 'foo',             },         ],     }

Запуск

Запускать примеры легче всего в докер-контейнере. Для этого нужно, чтобы на машине были установлены docker и docker-compose.

Все примеры запускаются из директории docs/examples

Запустить чат

# с хранилищем MongoDB docs/examples$ make run-chat-mongo  # с хранилищем PostgreSQL docs/examples$ make run-chat-postgres

После запуска в консоль будет выведен URL, по которому можно открыть чат в браузере:

chat-postgres_1 | ======== Running on http://0.0.0.0:8081 ======== chat-postgres_1 | (Press CTRL+C to quit)

Запустить тесты

# Выполнить тесты всех примеров docs/examples$ make docker-runtests
# Выполнить тесты отдельного примера docs/examples$ make docker-runtests-mockserver-example docs/examples$ make docker-runtests-mongo-example docs/examples$ make docker-runtests-postgres-example

Документация

Подробная документация testsuite доступна по ссылке.

Инструкция по настройке и запуску примеров.

Если есть вопросы github.com/yandex/yandex-taxi-testsuite/issues — оставьте комментарий.

ссылка на оригинал статьи https://habr.com/ru/company/yandex/blog/491038/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *