company_banner

Яндекс открывает 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 — оставьте комментарий.
    Яндекс
    Как мы делаем Яндекс

    Комментарии 28

      –2
      Ради такого можно и Питон начать учить…
        +2
        Python элегантный язык, приятно изучать и пользоваться :)
          0
          по количеству минусов так и не скажешь
        +18

        Ой, прям рискую кармой печатая этот коммент, но удержаться уж не в силах ибо, признаться честно, неравнодушен я к питону и инструментам для тестирования написанным на нём и для него. Кстати, когда читаю, что кто-то начал писать свой фреймворк для тестирования, то сразу в голову закрадывается мысль, что кому-то просто стало скучновато и он не осилил нормальных инструментов… только не берите последнее изречение на свой счёт, буду рад если я всё таки ошибаюсь. Скажу сразу — описываемое поделие не пробовал, так как сознаться, не очень понял из статьи зачем оно мне может понадобиться и как его правильно готовить.


        Итак, по порядку:


        Он предоставляет готовые механизмы, чтобы...

        Не тянет тезисной описание на список фич фреймворка. Не очень понятно зачем он нужен как отдельная сущность, а не как горсть плагинов для пайтеста или как отдельный мок-сервис. Вообще, солянка какая-то: первый тезис описывает http-клиент, второй мок-сервис, в третьем все про тот же мок-сервис(?), всё что написано в четвёртом — можно сделать yield фикстурами пайтеста.


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

        А зачем вам вообще это надо? Кстати, вы базу локально запускаете? Почему не запустить через докер и не залить фикстуркой? Докер пинать, к слову, можно тоже из питона.


        Решать эту задачу, пользуясь фреймворками для unit-тестов, слишком трудно и неправильно

        Вот тут надо уточнять, что вы вкладываете в эти понятия и почему неправильно.


        Можно запускать и на каждый тест (убрать scope='session'), но это медленно

        Такие комментарии в примерах кода только захламляют его, так как где-то выше по тексту писалось, что читатель должен быть знаком с пайтестом.


        os.getenv('PYTHON3', 'python3')

        Что это за чудо-переменная?


            python_path = os.getenv('PYTHON3', 'python3')
            service_path = pathlib.Path(__file__).parent.parent

        Это грязь. Я бы передавал путь к приложения через переменную окружения.


        Команда запуска сервиса. Первый элемент массива — исполняемый файл,

        Во-первых, не массива, а списка. Во-вторых, крайне не рекомендую такой способ инициализации чего-либо, если допустимо использование с питоном версии меньшей чем 3,7.


        service_daemon,, HTTP-статус ответа == 204

        В примере, вставленном в статью синтаксическая ошибка


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

        Погодите! В начале же обещали, что тесты смогут "поднимать и наливать базу данных".


        tests_dir = pathlib.Path(file).parent
        sqldata_path = tests_dir.joinpath('../schemas/postgresql').

        Грязь. Для примера кода к статье не допустимо. Вам бы завести какой, прости господи, файлик settings.py и в него поскладывать константы с путями и пр.


        Проверим, что в теле ответа JSON с идентификатором сохранённого сообщения

        Напрашивается валидация тела ответа через marshmallow.


        Запускать примеры легче всего в докер-контейнере.

        Погодите, так вы все таки умеете готовить докер и все работает через контейнеры? А как же "запускать в этом окружении тестируемый сервис". Так все таки все работает через контейнеры или в одном окружении? Из статьи крайне не ясно.


        make run-chat-mongo

        Я вас умоляю… Зачем пинать композ через мэйк?


        По коду:


        • асинхронщина в тестах всегда должна быть оправдана. Зачем оно вам, и тем более, в демонстрационном примере для статьи?
        • по коду куча констант и в том числе строковых. Magic number же?
        • не используется typing от слова совсем. В случае с пайтестом, который любит передавать фикстуры аргументом функции тайп хинтинг сильно облегчает жизнь и позволяет дышать полной грудью.
        • очень много работы с относительными путями, что само по себе не айс
        • обычный assert  очень не информативен — к нему в таких тестах надо либо подписывать сообщение, либо использовать какой-либо матчер вроде hamcrest
        • субъективно, коду примера не хватает хорошего ревью
          0
          Благодарю за подробный комментарий!

          асинхронщина в тестах всегда должна быть оправдана. Зачем оно вам, и тем более, в демонстрационном примере для статьи?

          Асинхронные обработчики нужны, например, чтобы моксервер отвечал на запросы, пока тест ожидает ответ сервиса, без асихнронности никак.

          по коду куча констант и в том числе строковых. Magic number же?

          Бывает, особенно часто в предусловиях и проверках, что код оказывается проще и понятнее, если не выделять константу.

          очень много работы с относительными путями, что само по себе не айс

          Согласен, но это необходимо написать только один раз. Обычно мы используем кодогенерацию и пути подставляются на этапе cmake.

          не используется typing от слова совсем. В случае с пайтестом, который любит передавать фикстуры аргументом функции тайп хинтинг сильно облегчает жизнь и позволяет дышать полной грудью.

          Действительно, значительная часть кода ещё не покрыта аннотациями, это наследие тех времён, когда проект работал на python2.7/gevent. Мы постепенно добавляем аннотации в имеющийся код и используем их в новом.

          * обычный assert очень не информативен — к нему в таких тестах надо либо подписывать сообщение, либо использовать какой-либо матчер вроде hamcrest

          pytest патчит assert-ы таким образом, что к ним можно подключить свои собственные обработчики, получается довольно удобно.

          Погодите! В начале же обещали, что тесты смогут «поднимать и наливать базу данных»

          Сервис chat-backend не взаимодействует с базой, за это отвечает другой сервис, chat-storage-postgres, вот когда мы тестируем chat-storage-postgres, тогда и наливаем базу.

          Погодите, так вы все таки умеете готовить докер и все работает через контейнеры? А как же «запускать в этом окружении тестируемый сервис». Так все таки все работает через контейнеры или в одном окружении? Из статьи крайне не ясно.

          Действительно, testuite может поднимать базу самостоятельно и «запускать в этом окружении тестируемый сервис», а может воспользоваться и готовым окружением, в том числе поднятым в docker, что удобно в CI. Мы в Яндекс.Такси используем оба решения.

          Не тянет тезисной описание на список фич фреймворка. Не очень понятно зачем он нужен как отдельная сущность, а не как горсть плагинов для пайтеста или как отдельный мок-сервис. Вообще, солянка какая-то: первый тезис описывает http-клиент, второй мок-сервис, в третьем все про тот же мок-сервис(?), всё что написано в четвёртом — можно сделать yield фикстурами пайтеста.

          Это введение, мы стремимся дать примерное представление, более целостное картину дают последующие разделы статьи.

          Вот тут надо уточнять, что вы вкладываете в эти понятия и почему неправильно.

          Потому что они прендазначены для решения другого типа задач. Статья про testsuite, а не юнит тесты, поэтому здесь мы не будем углубляться.

          Что это за чудо-переменная?

          Тут есть несколько вариантов, как это обойти: можно передавать пути через аргументы, через переменные окружения или вычислять их на ходу. В данном случае мы добивались того, чтоб тесты запусальись без указания дополнительных параметров. PYTHON3 необходима для того, чтобы можно было запускать тесты в окружении отличном от окружения самого testsuite.

          А в общем: согласен, что не очень красиво — поправим.

          Во-первых, не массива, а списка. Во-вторых, крайне не рекомендую такой способ инициализации чего-либо, если допустимо использование с питоном версии меньшей чем 3,7.

          В самом деле, list :)

          В примере, вставленном в статью синтаксическая ошибка

          Спасибо, исправили.

          Я вас умоляю… Зачем пинать композ через мэйк?

          С помощью make-файла мы показываем, как можно запустить нужные команды, мы не настаиваем на использование make.

          Напрашивается валидация тела ответа через marshmallow.

          Мы стремимся, насколько возможно, не навязывать пользователю выбор инструментов, поэтому testsuite предоставляет базовую функциональность. Поддержку валидации, скажем через marshmallow, можно добавить в своем проекте. Например, в Яндекс.Такси все ручки описаны с помощью yaml-схем, поэтому дополнительная валидация нам в этом месте не очень полезна.
            +2
            Асинхронные обработчики нужны, например, чтобы моксервер отвечал на запросы, пока тест ожидает ответ сервиса, без асихнронности никак.

            Субъективно, вы просто переинженерили решение, которому в рамках данного проекта вообще не место. И в итоге теперь надо через одно место писать сами тесты. И я правильно понимаю, что теперь и тем кто хочет использовать вашу балалайку везде надо будет забавляться с асинкоайо даже если оно им и не нужно? Можно было сделать с мок-сервером сильно более лучшее.


            Бывает, особенно часто в предусловиях и проверках, что код оказывается проще и понятнее, если не выделять константу.

            В таком количестве как у вас оно не бывает оправданным. Магическое значение есть магическое значение, тем более когда их много и они разбросаны по коду. Если вы считаете, что в тестах можно говнокодить в отличии от боевого кода, ну что уж тогда вообще разговаривать.


            Согласен, но это необходимо написать только один раз. Обычно мы используем кодогенерацию и пути подставляются на этапе cmake.

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


            Например, в Яндекс.Такси все ручки описаны с помощью yaml-схем, поэтому дополнительная валидация нам в этом месте не очень полезна.

            У вас доверительное тестирование чтоль? А если разработчик налажает в схеме и допустит ошибку?

              +1
              И я правильно понимаю, что теперь и тем кто хочет использовать вашу балалайку везде надо будет забавляться с асинкоайо даже если оно им и не нужно?

              Без async / await не обойтись в коде тестов. Если под везде подразумевается что-то ещё кроме кода тестов, то нет, не везде. В частности, сервис не обязан использовать async, а может вообще быть написан на другом языке, скажем C++.

              В таком количестве как у вас оно не бывает оправданным. Магическое значение есть магическое значение

              Я готов согласиться, что где-то магические строки / числа по коду не нужны, но, ещё раз, это верно не всегда. Часто бывает так, пишешь вначале тест с константами, получается лаконично, но сходу не понятно, что проверяется. Заменяешь константы на значения, и стало понятнее.

              У вас доверительное тестирование чтоль? А если разработчик налажает в схеме и допустит ошибку?

              Конечно нет, разработчик должен выписать ожидаемый в ответе json отдельно, но речь о другом. Мы не навязываем конкретный способ, которым это будет сделано. Можно просто сравнить dict в питоне, можно прикрутить фреймворк на ваш вкус.
                –2
                Без async / await не обойтись в коде тестов

                Приехали

          +3

          Долго думал и решил добавить еще пять копеек. Бегло посмотрел на код самого фреймворка. Во-первых, это не тянет на фреймворк. Мне понравилось, как говорил небезызвестный Бобук в радиоте, что фреймворк, это некая сущность которая обязывает тебя писать логику особым образом, а здесь очевидно, что это не так. В этом как бы ключевое отличие фреймворка от библиотеки. Допускаю, что фразу за давности лет мое сознание могло исказить. Хотя, конечно, здесь можно поспорить. Этот проект, скорее, набор разносольных инструментов/плагинов для упрощения своих задач при работе с пайтестом, на каком-то конкретном проекте.


          По коду:


          Если уж вы его причесали форматером с дефолтными настройками, а судя по всему это было что-то вроде yapf -i -r yandex-taxi-testsuite/testsuite, то надо было хотя бы допустить размер строки до 120 символов, дабы не плодились попусту уродские переносы строк.


          В коде очень часто встречается странное и с этим надо бороться. Глазам регулярно больно смотреть. Например такое:


                     try:
                          decoded = line.decode('utf-8')
                      except UnicodeDecodeError:
                          logger.error(
                              'Failed to decode subprocess output', with_exc=True,
                          )
                          continue

          Вот так делать нельзя в публичном коде, да и в обычном. Пролюбите строчку какую важную. Сделайте line.decode('utf-8', errors='replace') и удалите вон того Франкенштеина, что выше. Из кода также надо вычистить всякие дефолты в ноль для значения портов (как это вообще возможно?) и отрефачить код. Вообще, все те же магические значения повсюду, плоховатая организация кода, не указан тайп хинтинг даже для возвращаемых данных фикстур. Некоторые вещи можно было бы обмотать аллюровыми украшательствами раз уж он прижился у всех в тестировании. Проект забахан совсем недавно и одним коммитом => не ясна его дальнейшая судьба.


          Без обид только ребят, но как код используемый внутри организации это большая работа и несомненно нужная. Я бы сказал даже, что это нормальное такое решение для тестирования для отдельно взятого проекта. Но как публичный проект — громоздкий, непонятно зачем нужный кому-то кроме вас, умоляющий о декомпозиции и не то чтоб грязноватый, но скажем так — если принюхаться, то от него пахнет кровавым энтерпрайзом у которого не хватает времени на то чтоб подвести губки собственному проекту прежде чем выставить его на всеобщее обозрение.

            –2
            Если уж вы его причесали форматером с дефолтными настройками, а судя по всему это было что-то вроде yapf -i -r yandex-taxi-testsuite/testsuite, то надо было хотя бы допустить размер строки до 120 символов, дабы не плодились попусту уродские переносы строк.

            Мы форматируем код по pep8, с длиной строки в 79 символов.

            Вот так делать нельзя в публичном коде, да и в обычном. Пролюбите строчку какую важную. Сделайте line.decode('utf-8', errors='replace') и удалите вон того Франкенштеина, что выше.

            В данном случае мы форматируем строку для записи в лог, а если не смогли раскодировать, то логируем ошибку, из которой будет видно, в чем дело.

            Из кода также надо вычистить всякие дефолты в ноль для значения портов (как это вообще возможно?) и отрефачить код.

            0 означает, что операционная система выберет любой порт. Это стандартное значение для bind(2).
            Проект забахан совсем недавно и одним коммитом => не ясна его дальнейшая судьба

            Но как публичный проект — громоздкий, ...

            Проект действительно enterprise, появился и развивается решая конкретные задачи. Это верно сейчас, это будет верно и в будущем, когда проект будет и декомпозирован, и почищен, и так далее, будем заниматься этим, обязательно.
              +2
              Мы форматируем код по pep8, с длиной строки в 79 символов.

              Это походу самое часто игнорируемое правило. Вам самим-то приятно на такое смотреть:


              logger.error(
                                  'Failed to decode subprocess output', with_exc=True,
                              )

              Плюс форматеры тоже надо настраивать чтоб таких не выходило:


                  def __init__(
                          self,
                          base_url,
                          *,
                          mocked_time,
                          span_id_header=None,
                          raw_response=False,
                          **kwargs,
                  ):

              ?


              Если уж вы договорились, что строго по пепе живете, то и код надо писать компактненько, а не отдавать все на откуп форматеру.


              В данном случае мы форматируем строку для записи в лог, а если не смогли раскодировать, то логируем ошибку, из которой будет видно, в чем дело.

              Действительно, при UnicodeDecodeError ведь столько вариантов проблем...


              Проект действительно enterprise, появился и развивается решая конкретные задачи. Это верно сейчас, это будет верно и в будущем, когда проект будет и декомпозирован, и почищен, и так далее, будем заниматься этим, обязательно.

              Ох, ребят )) Здесь койки двигать уж себе дороже будет.

                –2
                В самом деле, автоматически отформатированный код не так же приятен глазу, как тщательно продуманное ручное форматирование.

                В данном случае по шкале Максимальная элегантность <----> Минимальное время, затраченное на форматирование сделано предпочтение в пользу скорости. Когда я добавляю новый код, мне это не нравится, но когда приходится вносить большие правки в чужой код, править кофликты слияний и т.п. то наоборот, нравится.

                Когда над кодовой базой работают сотни людей, возможность отформатировать правки автоматически является решающей для того, чтобы разрешать конфликты с новыми правками быстрее, чем появляются новые.
                  +2
                  В самом деле, автоматически отформатированный код не так же приятен глазу, как тщательно продуманное ручное форматирование.

                  Не согласен. Через задницу настроенный форматер это не беда инструмента, а изъян команды, боль глазам, (в данном случае) куча избыточных строк и как результат — низкое качество кода.

                    –1
                    Что же, мы расходимся в оценке того, хороши или плохи некоторые настройки автоформатирования. Я нахожу те настройки, которые есть удовлетворительными. Я и сам мог бы указать на те или иные хотелки, которые сделаны не так, как мне хотелось бы больше всего, однако на всех сразу не угодишь. Во всяком случае, такого ужаса как вы привели выше

                    logger.error(
                                        'Failed to decode subprocess output', with_exc=True,
                                    )


                    они не делают, это был просто кривой копипаст, в действительности бывает такое

                    logger.error(
                        'Failed to decode subprocess output', with_exc=True,
                    )
                      0

                      Главная претензия ни к кривым скобкам была, а в общем-то в том, что не понятно зачем простой крохотный лог занимает три строки кода.

              +1
              применяется в Яндекс.Такси


              Интересно что используют другие сервисы Яндекса, и почему не этот фреймворк? Не подошло?
                0
                Помимо Яндекс.Такси, мы используем testsuite при разработке некоторых сервисов Яндекс.Лавки. Основной потребитель всё ещё Яндекс.Такси, потому что именно в Такси разработали testsuite, мы изначально используем его для всех сервисов.
                0

                Основа — pytest. Молодцы, никаких велосипедов там, где не надо.


                … С точки зрения эстетики (я поверхностно смотрю) — оно не выглядит сильно приятнее, чем (условный) betacam с кассетами ответов. Почему так много и так некрасиво?

                  0
                  Не понял, это был риторический вопрос или что?
                    +1

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

                      +3
                      В контексте некоторой глубины, при которой вопрос не риторический, не могли бы вы уточнить, чего много и некрасиво?
                  0
                  Будет ли статья на тему «Интеграционные тесты testsuite»?
                    0
                    Таких планов не было, но идея интересная, подумаем.
                      0

                      Оказывается, есть доклад про релизный цикл, там уже написано про интеграционные тесты Яндекс.Такси.

                      0
                      йоу!
                      а можешь рассказать насколько производительный фреймворк и какое кол-во запросов в секунду удавалось достичь в пике?
                        0
                        assert 'id' in data
                        возможно я не прав, но мне кажется правильнее
                        assert data['id']
                        В Вашем случае тест пройдет, если в json есть id, но он пуст.
                          0

                          согласен

                          0
                          Есть другая интересная штука для интеграционных тестов: github.com/testcontainers

                          Для python тоже есть порт.

                          И вот интересный доклад по теме: www.youtube.com/watch?v=PEVVvZOt7bY

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.