Robot Framework vs Pytest

    Я активный сторонник Robot Framework. Уже писал на Хабре о том, что с его помощью можно решить практически любую задачу по автоматизации тестирования, особенно когда разработка ведется на Python. В той же статье я упоминал, что на смежных проектах в компании используется Pytest. Мне пришлось довольно близко познакомиться с этим инструментом, так что теперь я готов провести его полноценное сравнение с Robot Framework, конечно же, со своей персональной колокольни.

    Robot vs. Snake by Beanhex (https://www.weasyl.com/~beanhex)
    Robot vs. Snake by Beanhex (https://www.weasyl.com/~beanhex)

    Кстати, на идею статьи меня натолкнул один из комментариев к предыдущей статье, в котором читатель сравнил jUnit и Robot Framework для проекта на Java. На мой взгляд, в этих условиях Robot Framework (основанный на Python) изначально был не очень применим. Но сама идея сравнения ближайшего аналога jUnit в мире Python (того самого Pytest) и Robot Framework мне понравилась.

    Что это за инструменты?

    Pytest

    Фактически, Pytest - реализация xUnit фреймворка для Python. Если вы всю жизнь работали с jUnit или nUnit (двумя самыми распространенными представителями этого семейства для Java и .NET соответственно), то в Pytest найдете ровно те же стандарты - быстро поймете, что происходит, будете следовать привычным подходам написания юнит-тестов. xUnit-фреймворки - это надстройки над языком программирования или библиотеки в них, которые удобно использовать самим разработчикам. За счет этого они довольно распространены и популярны. 

    Для понимания ситуации вместе с Pytest, как и вместе со многими xUnit-фреймворками, необходимо использовать инструмент генерации логов. Уже традиционно в этой роли применяется Allure. Наверное, сегодня это стандарт для логов автотестов, который хочет видеть бизнес. Но надо понимать, что Pytest разрабатывается своим сообществом, а Allure - своим, и векторы этих команд могут в какой-то момент развернуться в разные стороны.

    Robot Framework

    В отличие от Pytest, Robot Framework - это domain specific language (DSL) - язык, специфичный для своей предметной области. Это не Python, хотя он на нем построен. Зато на Python можно написать все, что угодно для Robot Framework. И все возможности, о которых я здесь говорю (а также интеграции, настройки), доступны из коробки.

    Robot Framework не настолько удобен разработчикам, поскольку требует более глубокого погружения. По сути, это другой подход написания автотестов. Как Cucumber для Java. Зато Robot Framework самостоятельно генерирует развернутые отчеты (мы об этом еще поговорим), т.е. фактически он содержит в себе также слой генерации отчетов, и развивается продукт одним сообществом - неделимо и гармонично. 

    Кстати, сообщество открытое и довольно отзывчивое. У него есть открытый канал в Slack, где каждый желающий может задать вопрос по Robot Framework и получить на него ответ. Я и сам иногда там отвечаю. Я даже иногда контрибьючу в Robot Framework.

    Аргументы в пользу Robot Framework

    Отсутствие лишних ограничений на наименования

    В Pytest и ряде других xUnit фреймворков название тест-кейса всегда должно начинаться с test. Мне кажется логичным, когда используется метод test. Но в остальных случаях ограничения на наименования не помогают процессу. 

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

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

    Robot Framework никак не заточен на юнит-тесты. В нем предусмотрен раздел Keywords, а сами тесты могут называться как угодно. Мне кажется, так работать удобнее. Более того, в Robot Framework любые сущности, в том числе keywords, можно называть по-русски. Грамотно подобрав названия, текст теста можно превратить в рассказ (“Открой сайт, введи логин, убедись, что результат такой-то”). Это позволяет не писать длинные комментарии, рассказывающие о том, что там происходит. Любой участник разработки или руководитель, никогда не залезавший в недра тестирования, может открыть этот тест и понять его. И время экономится, и понимание ситуации даже через полгода сохраняется. 

    Suite setup

    Бывает, что для набора тест-кейсов (тест-сьюта) требуются однотипные настройки, например выполнение метода, который готовит некие данные, создает сущности и сохраняет их идентификаторы для использования в тестах. В Robot Framework предусмотрены отдельные настройки suite setup, которые применяются для всех тестов в наборе. Отдельно есть suite teardown, который выполняется после всех тестов в наборе, а также аналогичные настройки для каждого тест-кейса (test setup и test teardown). Это удобно, логично и не требует изобретения велосипедов.

    В xUnit фреймворках есть аннотации, заменяющие настройки suite setup, а в Pytest есть фикстуры со scope=”class”

    В Pytest тест-кейсы могут быть просто методами (и тогда у них нет никакого suite setup - т.е. нет единой подготовки). Если же нужна единая подготовка, мы можем обернуть эти методы в класс. Но если мы сделаем фикстуру со scope=”class” для этого класса (т.е. попытаемся реализовать suite setup), то получим отдельный инстанс класса для каждого теста, так что данные из suite setup никак не попадут в тест-кейсы. Отдельные инстансы, вероятно, создаются из предположения, что данные из разных тест-кейсов не должны влиять друг для друга. Но из-за этого настроить среду для выполнения тестов намного сложнее, чем в Robot Framework, где suite setup предусмотрен априори.

    Обычно этот вопрос в Pytest приходится обходить через создание отдельной фикстуры, куда загружаются данные. Эта фикстура впоследствии подтягивается в тесты. Другой путь - воспользоваться средствами Python, создав статическое поле у класса, которое будет общим для всех его инстансов (например, self.__class__.test_id = 2). Но на мой взгляд, это тоже костыль - к полям класса через подчеркивание не стоит обращаться извне.

    Логи

    Как я отметил выше, в Pytest для генерации логов чаще всего используется Allure. Это красиво и модно. Но из-за описанной выше особенности с инстансами, Allure не понимает, что и как связано с тестом. В отчет не попадают действия из suite setup. Приходится это обходить через написание обработчиков. И с моей точки зрения это уже не просто костыль, а настоящий костылесипед. 

    Кстати, в других xUnit фреймворках эта проблема тоже есть.

    На фоне Pytest+Allure у Robot Framework логи максимально подробны и даже избыточны. Они включают даже то, о чем ты никогда не задумаешься - Robot пишет все, что ты делаешь. С помощью этого лога гораздо проще отлавливать плавающие ошибки, которые так просто не воспроизведешь. Ты точно знаешь, где и какое значение было у переменной, какой API вызвали. Зачастую и перезапускать тест не надо, чтобы понять, что происходит. Для Pytest в таких сложных случаях приходится придумывать инструменты, которые помогают генерировать лог, как у Robot Framework.

    Синтаксический сахар

    В Robot Framework не нужно придумывать усложняющих конструкций. Есть выражения, спасающие от лишнего кода. 

    Я люблю писать keyword-обертки и в них оборачивать другие keyword-ы. Например, в keyword, который из ответа API берет ID, можно будет подсовывать keyword, который дергает разные API (если в компании есть стандарты написания кода, то ответы API будут сходными - поле ID скорее всего будет во всех).

    Например, можно написать: “Идентификатор из создание сущности”. Здесь “Идентификатор из” - keyword-обертка, “создание сущности” - это keyword, который вызывает API и отдает весь ответ. В другом случае можно написать “Идентификатор из создание таблицы”, где “создание таблицы” - это другой keyword.

    Для меня писать подобным образом удобнее и понятнее. А если изменить keyword с “создание сущности” на “создания сущности”, то конструкция будет читаться даже с литературной точки зрения (“Идентификатор из создания сущности”).

    Теггирование

    В заголовке лога Robot Framework показывает статистику по каждому тегу. Если изначально правильно расставить теги, достаточно будет взглянуть на эту статистику, чтобы увидеть, в чем проблема, не забивая себе голову тем, что происходит внутри. Я писал про это в предыдущей статье. Там же я упоминал, что теги можно привязывать к идентификаторам багов в Jira или в любом другом треккере, который у вас используется. Как раз недавно у меня был случай, когда на одном из поддерживаемых тестов, о которых я и думать забыл с полгода назад, “сработали” старые забытые баги, привязанные к тегам. И на их поиски ушло несколько секунд, вместо минут и часов.

    В Pytest присутствует теггирование, но оно не попадает в лог. Снова нужны какие-то костыли, чтобы реализовать это своими руками. Поэтому, насколько я знаю, так почти никто не делает. 

    Кстати, и Allure не дает возможности отобразить статистику по тегам. Вероятно, если бы в нем появилась такая возможность, она в моих глазах приблизила бы связку Pytest+Allure к Robot Framework по функциональности. Плюс был бы в том, что связка Pytest+Allure не требовала бы забивать головы консервативных разработчиков новым DSL. К сожалению, появление таких инструментов маловероятно из-за того, что Pytest и Allure развиваются разными группами.

    Аргументы в пользу Pytest

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

    Отладка

    Об этом писал один из читателей в комментариях к предыдущей статье. К сожалению, ты не можешь приостановить выполнение в Robot Framework, чтобы посмотреть значения переменных. Это факт, который можно записать в плюс Pytest.

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

    Параметрические тесты Pytest

    Я нашел то, чего нет в Robot Framework - благодаря параметризации Pytest может создавать тест-кейсы на лету. В Robot Framework 10 тестов всегда будут 10-ю тестами, ни больше, ни меньше. В Pytest в некоторых случаях с помощью одного параметрического теста можно покрыть огромное количество кейсов.

    На примере реального проекта. Предположим, у нас есть API с двумя параметрами, каждый из которых может принимать несколько вариантов значений (например, один принимает 7 значений, другой - 10). И эти значения друг друга не исключают. В соответствии с теорией тестирования в таком случае надо выбрать несколько кейсов, более-менее равномерно покрывающих сетку из 70 “пересечений” (метод  pair-wise). Но я с помощью метода product из модуля itertools (который перемножает списки) написал тест-сетап, который подготавливает 70 комбинаций данных, а потом ходит в API и обеспечивает то самое утопическое exhaustive testing. При появлении еще одного варианта в начальных данных мне достаточно просто добавить строчку в один из первоначальных списков.

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

    В целом плюсов за Robot Framework получилось больше - я остаюсь его ярым сторонником. Но, конечно, это лишь мое мнение.

    Автор статьи: Владимир Васяев

    P.S. Мы публикуем наши статьи на нескольких площадках Рунета. Подписывайтесь на наши страницы в VK, FB, Instagram или Telegram-канал, чтобы узнавать обо всех наших публикациях и других новостях компании Maxilect.

    Maxilect
    Умные решения для вашего бизнеса

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

      0

      В плюсы RF стоит записать также отличную документацию. Она действительно очень хорошо написана. Помню, как сам погружался во фреймворк несколько лет назад: скачал мануал, распечатал, взял с собой в междугородний автобус — и через 4 часа чтения уже нормально понимал, что и как в нём надо делать (либо хотя бы где именно в документации искать то, что мне потребуется). Мало какая библиотека сравнится с ним по этому параметру.


      В рабочем проекте мы достаточно продуктивно использовали RF, но потом всё равно с него съехали. Причин было несколько, и все их тут вряд ли получится описать. Самая главная — разработчики писали проект на Java, и хотели что-то более близкое по языку (выбрали Spock в итоге). Но своё дело он сделал, и опыт работы с ним до сих пор приятно вспомнить.


      С другой стороны, лично я в своих Python-проектах последних лет однозначно делал выбор не в его пользу, а всегда брал pytest. Оправдано ли это? Надо подумать....

        0
        По-моему, эти два фреймворка некорректно сравнивать — pytest, это прежде всего более удобная замена юнит-тестам. И дело не в самом интерфейсе, а в том, что предполагается тестировать довольно низкоуровневые штуки. Хотя я не работал с robotics framework, насколько я понимаю, он заточен прежде всего для удобного описания пользовательского поведения во время тестирования (и порой, довольно сложного).
        Что касается следующего:
        В xUnit фреймворках есть аннотации, заменяющие настройки suite setup, а в Pytest есть фикстуры со scope=”class”.

        В Pytest тест-кейсы могут быть просто методами (и тогда у них нет никакого suite setup — т.е. нет единой подготовки). Если же нужна единая подготовка, мы можем обернуть эти методы в класс. Но если мы сделаем фикстуру со scope=”class” для этого класса (т.е. попытаемся реализовать suite setup), то получим отдельный инстанс класса для каждого теста, так что данные из suite setup никак не попадут в тест-кейсы. Отдельные инстансы, вероятно, создаются из предположения, что данные из разных тест-кейсов не должны влиять друг для друга. Но из-за этого настроить среду для выполнения тестов намного сложнее, чем в Robot Framework, где suite setup предусмотрен априори.

        То кажется, что вы просто не поняли суть scopes. Они нужны не для того, чтобы указать, на какие тесты выполнять сетап (связи scope и применимости фикстур нет), речь идет о том, что иногда нужно выполнять код, и кешировать результат (т.е. не выполнять этот код на каждый тест в классе). Применимость этой ситуации довольно ограниченная, т.к. по-умолчанию считается, что все инициализации для каждого теста нужно делать с чистого листа, но в некоторых ситуациях (типа создаем соединения со сторонним сервисом, или запускаем сервер), альтернативы нет. Именно для этого и нужно указывать scopes, на практике, я ни разу не использовать scope с уровнем класса, обычно или используется дефолтный scope функции, либо сразу всей сессии целиком.
        Чтобы применить одну и ту же фикстуру на класс, можно воспользоваться декоратором
        @pytest.mark.usefixtures("yourfixturename")
        class TestClass(TestCase):
             pass

        В этом случае, фикстура будет применена на все методы тесткейса, но инициализироваться она будет каждый раз с чистого листа. При этом, вовсе нет необходимости засовывать эти тест функции в класс в виде методов — с таким же успехом можно указать нужную фикстуру явно.
        Если же, по каким-то причинам, явное указание не приемлемо (к примеру, потребуется отрефакторить сотню тестов), фикстуру можно применить автоматически внутри конкретного модуля, либо даже пакета:
        @pytest.fixture(autouse=True)
        def my_fixture():
            pass

        В целом, чтобы понять парадигму pytest, нужно разобраться с возможностями фикстур, т.к. благодаря им, переиспользования кода выполняется при помощи композиции объектов, вместо наследования.
          +1
          Теперь по поводу, чем же крут pytest, но сравнивать я буду с unittest интерфейсом (это будет более корректно).
          Проблема тестовых классов в том, что setUp/tearDown порой становятся тяжелыми, и весь зоопарк объектов и моков запускается для всех тестовых методов класса:
          Скрытый текст

          Т.е., если у вас есть один маленький тестовый метод, для которого нужна половина этих объектов, все-равно будет вызван весь setUp и tearDown.
          Pytest позволяет избежать этого, предлагая явно указывать только те фикстуры, которые нужны:
          Скрытый текст

          Если вы захотите переиспользовать setUp/tearDown между классами, то ситуация только усугубится, т.к. теперь вы будете создавать объекты из родительских классов:
          Скрытый текст


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


          При этом, вы можете указывать зависимости фикстур одной от другой, объединяя их в цепь:
          Скрытый текст


          Но самое крутое, что фикстуры могут не только генерировать тестовые объекты, но еще и мокать библиотеки, используя синтаксис генераторов. При этом, фикстура в себе включает, как setUp часть, так и tearDown:
          Скрытый текст


          Т.е. фикстуры представляют полноценную замену setUp/tearDown методам, при этом элегантно избегая проблемы божественного класса.
          Есть еще и другие удобные плюшки в pytest, как параметрические тесты, локальные conftest конфигурации и плагины, но это уже больше детали.
          +1
          В Pytest и ряде других xUnit фреймворков название тест-кейса всегда должно начинаться с test. Мне кажется логичным, когда используется метод test. Но в остальных случаях ограничения на наименования не помогают процессу.

          Мне кажется эта идея довольно сомнительной, но если есть такая необходимость, то вы легко можете переопределить это поведение в конфигурации:
          # content of pytest.ini
          # Example 1: have pytest look for "check" instead of "test"
          [pytest]
          python_files = check_*.py
          python_classes = Check
          python_functions = *_check
            0
            Поскольку Pytest заточен под юнит-тестирование, реальные и тестовые методы приложения могут лежать в одном классе, так что возможна неразбериха.

            Не должны! Тесты всегда лежат отдельно от кода. Но что касается pytest, то его интерфейс направлен на то, чтобы писать отдельные тестовые функции, а не классы. Поддержка классов нужна прежде всего для обратной совместимости с олдскульными юниттестами.
              0
              Сорян, что несколькими коментариями, так проще отвечать :)
              0
              pytest – это инструмент, который подходит для различных видов тестирования, а не только юнит. Благодаря расширяемой архитектуре на основе pluggy, он позволяет решать практически любые задачи. Если есть опыт работы с pytest, то считаю, что связка pytest + pytest-bdd + allure будет более сильной.
                +1

                Пользовались на нескольких проектах когда-то роботом, совершенно не понравилось. Если мне память не изменяет, любой нетривиальный кейс превращался в нечитаемую портянку из текста на роботовском dsl и имплементации ключевых слов на питоне, между которыми приходилось постоянно переключаться. И нормальной поддержки в IDE тогда не было (сейчас может есть, не знаю).
                Я так понимаю, что основная идея робота была в том, что тесты на нем смогут писать тестировщики, не умеющие писать код, но с нашими тестировщиками это явно не работало. Оказалось проще их обучить программированию.

                  +1
                  К сожалению, ты не можешь приостановить выполнение в Robot Framework, чтобы посмотреть значения переменных.

                  Это вполне возможно, например, в связке Language Server Protocol для Robot Framework и VS Code

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

                  Самое читаемое