В этой статье вы можете найти наглядное сравнение двух популярных инструментов для автоматизации тестирования: PyTest и RobotFramework.
На Хабре уже есть хорошая статья с общим сравнением этих фреймворков. Я сфокусируюсь на простоте параметризации тестов.

В одной из хабр-статей про Robot Framework утверждалось, что там нельзя создавать тесты "на лету" как в PyTest.
Недавно пришлось решать именно эту проблему, поэтому хочу показать способ хоть и криво, но обойти это ограничение.
Начать, тем не менее предлагаю со сценариев, в которых Robot Framework и PyTest справляются одинаково хорошо. Фреймворком PyTest я пользовался только в образовательных целях, но в этой статье с ним всё максимально просто. Особых знаний PyTest не нужно, так как напрягаться чтобы повторить те же тесты приходится Robot Framework-y.
Параметризация
Если несколько тестов проверяют одну и ту же функцию передавая туда разные аргументы это выглядит довольно громоздко:
Тест1
Проверим что Сложение(0, 0) возвращает 0
Тест2
Проверим что Сложение(-1, 1) возвращает 0
…
Тест9
Проверим что Сложение(7, 8) возвращает 15
Вместо запуска одного и того же теста с разными входными данными удобнее сделать один тест, который будет принимать несколько наборов аргументов.

Это обычно и понимают под параметризацией.
Выглядит такой тест примерно так:
ПараметризированныйТест((0, 0, 0), (-1, 1, 0), ... , (7, 8, 15))
Чтобы избежать противоречий отметим следующий момент:
Параметризированный тест не склеивает тесты в один большой тест. Если было девять тестов их останется девять, проходить успешно или падать они будут независимо друг от друга.
Помимо лаконичности кода у параметризации есть ряд преимуществ разной степени уникальности.
Этот способ потом легче масштабировать. Также при наличии тяжёлых для вычисления шагов, их повтор плохо скажется на производительности.
Например, гораздо эффективнее обратиться к базе данных в начале теста, а в конце закрыть соединение, чем открывать и закрывать соединение для каждого набора данных.
Подготовка тестового окружения
Если вы собираетесь повторять примеры из этой статьи — подготовить окружение можно следующим образом
Сперва нужно создать виртуальное окружение.
python -m venv venv
Затем нужно активировать его.
В Windows команда выглядит следующим образом:
.\venv\Scripts\activate
В Linux — немного по‑другому:
source ./venv/bin/activate
Установить зависимости можно следующей командой:
python -m pip install pytest requests xmlschema robotframework robotframework-datadriver
Как вариант можно установить зависимости из requirements.txt
pytest==8.3.5
requests==2.32.3
xmlschema==3.4.3
robotframework==7.2.2
robotframework-datadriver==1.11.22
python -m pip install -r requirements.txt
Надеюсь, я ничего не забыл и можно переходить к примерам.
Пример 1. Трехзначные числа
Функция filter_three() из модуля three_digit.py должна возвращать True если число трехзначное.
def filter_three(x: int) -> bool: return 100 <= x < 1000
Протестируем её с помощью PyTest и RF.
Структура проекта:
filter_three/
├── three_digit.py
└── tests
├── pytest
| └── test_filter_three.py
└── robot
└── test_filter_three.robot
Когда мы проверяем функцию filter_three() мы будем передавать в нее как трехзначные так и не трёхзначные числа.
С каждым таким тестом придётся передавать и ожидаемый результат.
Пример 1. PyTest
Наши тестовые данные можно представить как список кортежей вида:
[
(Число, Ожидаемый результат),
(Число, Ожидаемый результат),
(Число, Ожидаемый результат)
]
Важно отметить, что здесь мы имеем фиксированный набор тестов. В данном примере их шесть.
Когда мы будем итерировать по тестовым данным нам, для распаковки кортежей понадобятся две переменные, назовем их test_input и expected_result
import pytest
from three_digit import filter_three
@pytest.mark.parametrize("test_input, expected_result", [
(8, False),
(99, False),
(100, True),
(101, True),
(999, True),
(1000, False)
])
def test_compare(test_input, expected_result):
res = filter_three(test_input)
assert res == expected_result
python -m pytest -v --no-header .\tests\pytest\test_filter_three.py
======================== test session starts ==========================
collected 6 items
tests/pytest/test_filter_three.py::test_compare[9-False] PASSED [ 16%]
tests/pytest/test_filter_three.py::test_compare[99-False] PASSED [ 33%]
tests/pytest/test_filter_three.py::test_compare[100-True] PASSED [ 50%]
tests/pytest/test_filter_three.py::test_compare[101-True] PASSED [ 66%]
tests/pytest/test_filter_three.py::test_compare[999-True] PASSED [ 83%]
tests/pytest/test_filter_three.py::test_compare[1000-False] PASSED [100%]
========================= 6 passed in 0.02s ===========================
Для более наглядной демонстрации результатов можно дать каждому тесту название, с помощью pytest.param id=
Это упростит сравнение с роботом, поэтому приём будет использоваться во всех тестах этой статьи.
import pytest
from three_digit import filter_three
@pytest.mark.parametrize("test_input, expected_result", [
pytest.param(8, False, id="Digit"),
pytest.param(99, False, id="Two Digit Number"),
pytest.param(100, True, id="Lower Border"),
pytest.param(101, True, id="Three Digit Number"),
pytest.param(999, True, id="Upper Border"),
pytest.param(1000, False, id="Four Digit Number")
])
def test_compare(test_input, expected_result):
res = filter_three(test_input)
assert res == expected_result
======================== test session starts ==========================
collected 6 items
tests/pytest/test_filter_three.py::test_compare[Digit] PASSED [ 16%]
tests/pytest/test_filter_three.py::test_compare[Two Digit Number] PASSED [ 33%]
tests/pytest/test_filter_three.py::test_compare[Lower Border] PASSED [ 50%]
tests/pytest/test_filter_three.py::test_compare[Three Digit Number] PASSED [ 66%]
tests/pytest/test_filter_three.py::test_compare[Upper Border] PASSED [ 83%]
tests/pytest/test_filter_three.py::test_compare[Four Digit Number] PASSED [100%]
========================= 6 passed in 0.02s ===========================
Пример 1. Robot Framework
В Robot Framework параметризация делается с помощью шаблонов.
*** Settings ***
Library ../../three_digit.py
Test Template Validate Number
*** Test Cases *** number expected_result
Digit 8 ${False}
Two Digit Number 99 ${False}
Lower Border 100 ${True}
Three Digit Number 101 ${True}
Upper Border 999 ${True}
Four Digit Number 1000 ${False}
*** Keywords ***
Validate Number
[Arguments] ${number} ${expected}
${res}= Filter Three ${number}
Should Be Equal ${res} ${expected}
python -m robot .\tests\robot\test_filter_three.robot
========================================================================
====== Test Filter Three
========================================================================
====== Digit | PASS |
------------------------------------------------------------------------
------ Two Digit Number | PASS |
------------------------------------------------------------------------
------ Lower Border | PASS |
------------------------------------------------------------------------
------ Three Digit Number | PASS |
------------------------------------------------------------------------
------ Upper Border | PASS |
------------------------------------------------------------------------
------ Four Digit Number | PASS |
------------------------------------------------------------------------
------ Test Filter Three | PASS | 6 tests, 6 passed, 0 failed
========================================================================
В этом примере большой разницы между фреймворками не выявлено.
Пример 2. Умножение двух чисел
Следующий пример — параметризации простого умножения. Он не очень сильно отличается от предыдущего. Разница в том, что здесь больше аргументов передается в тест. Изучая сперва Пример 1 а затем Пример 2 читателю, не знакомому с PyTest, проще разобраться с синтаксисом.
Если вы хорошо знакомы с PyTest можете переходить к следующему примеру.
Структура проекта:
parametrize/
├── prod
│ ├── prod.py
│ └── tests
│ ├── pytest
│ │ └── test_prod.py
│ └── robot
│ └── test_prod.robot
└── venv
# prod.py
def prod(a, b):
return a * b
Пример 2. PyTest
Здесь тестовые данные будут немного сложнее. Мы передаем список кортежей. В каждом кортеже находится два элемента. Первый это кортеж из двух умножаемых чисел. Второй — ожидаемый результат в формате int.
[
((Множитель, Множитель), Ожидаемый результат),
((Множитель, Множитель), Ожидаемый результат),
((Множитель, Множитель), Ожидаемый результат)
]
Чтобы не накручивать сложную распаковку, элементы кортежа с множителями перебираются с помощью *args.
# test_prod.py
import pytest
from prod import prod
@pytest.mark.parametrize(
"args, expected_result",
[
pytest.param((0, 0), 0, id="zero - zero"),
pytest.param((7, -8), -56, id="positive - negative"),
pytest.param((13.0, 14), 182, id="float positive - positive"),
])
def test_param_prod(args, expected_result):
res = prod(*args)
assert res == expected_result
Запускать тесты будем из директории prod командой.
python -m pytest -v --no-header .\tests\pytest\
====================== test session starts ======================
collected 3 items
tests/pytest/test_prod.py::test_basic_param_prod[zero - zero] PASSED [ 33%]
tests/pytest/test_prod.py::test_basic_param_prod[positive - negative] PASSED [ 66%]
tests/pytest/test_prod.py::test_basic_param_prod[float positive - positive] PASSED [100%]
==================== 3 passed in 0.01s =========================
Пример 2. Robot Framework
*** Settings ***
Library ../../prod.py
Test Template Validate Prod
*** Test Cases *** arg1 arg2 expected_result
Zero Zero ${0} ${0} ${0}
Positive Negative ${7} ${-8} ${-56}
Float Positive Positive ${13.0} ${14} ${182}
*** Keywords ***
Validate Prod
[Arguments] ${arg1} ${arg2} ${expected_result}
${product}= Prod ${arg1} ${arg2}
Should Be Equal ${product} ${expected_result}
python -m robot .\tests\robot\test_prod.robot
==================================================
====== Test Prod
==================================================
====== Zero Zero | PASS |
--------------------------------------------------
------ Positive Negative | PASS |
--------------------------------------------------
------ Float Positive Positive | PASS |
--------------------------------------------------
------ Test Prod | PASS | 3 tests, 3 passed, 0 failed
==================================================
И этот пример большой разницы между фреймворками не выявил. Но со следующего примера дела пойдут пободрее.
Пример 3. Квадратное уравнение
В этом примере мы будем тестировать скрипт, который получает на входе коэффициенты квадратного уравнения (ограничимся целыми числами) а на выходе возвращает корни.
Рассмотрим два варианта. В первом корни возвращаются как список, во втором — как кортеж. Сравним насколько сильно нужно будет изменить тесты, чтобы адаптироваться к переходу от списка к кортежу.
parametrize/
├── quad
│ ├── quadratic.py
│ └── tests
│ ├── pytest
│ │ └── test_quadratic.py
│ └── robot
│ └── test_quadratic.robot
└── venv
Запускать тесты будет из директории quad.
Пример 3.0 PyTest
Рассмотрим полный код теста:
# test_quadratic.py
import pytest
from quadratic import quadratic_solve
@pytest.mark.parametrize("args, expected_result", [
pytest.param((1, -3, -4), [4, -1], id="two roots",),
pytest.param((1, -2, 1), [1.0, None], id="single root",),
pytest.param((0, 0, 0), [None, None], id="no roots",)
])
def test_solution(args, expected_result):
res = quadratic_solve(*args)
assert res == expected_result
python -m pytest --no-header -v tests/pytest/test_quadratic.py
============================ test session starts ===============================
collected 5 items
tests/test_quadratic.py::test_raises_type_error PASSED [ 20%]
tests/test_quadratic.py::test_result_is_tuple PASSED [ 40%]
tests/test_quadratic.py::test_solution[two roots] PASSED [ 60%]
tests/test_quadratic.py::test_solution[single root] PASSED [ 80%]
tests/test_quadratic.py::test_solution[no roots] PASSED [100%]
============================== 5 passed in 0.01s ==============================
Как можно увидеть при использовании опции ‑v
все три теста [two roots]
, [single root]
, [no roots]
успешно пройдены.
Пример 3.0 RobotFramework
Параметризация по‑прежнему будет осуществляться с помощью шаблонов.
Роботу нужна небольшая подготовка данных в разделе Variables. Если знаете способ упростить эту часть — пишите в комментариях.
# test_quadratic_lists.robot
*** Settings ***
Library ../../quadratic_lists.py
Test Template Verify Solution
*** Variables ***
@{roots1}= ${4.0} ${-1.0}
@{roots2}= ${1.0} ${None}
@{roots3}= ${None} ${None}
*** Test Cases *** a b c expected_result
Two Roots ${1} ${-3} ${-4} ${roots1}
Single Root ${1} ${-2} ${1} ${roots2}
No Roots ${0} ${0} ${0} ${roots3}
*** Keywords ***
Verify Solution
[Arguments] ${a} ${b} ${c} ${expected}
${res}= Quadratic Solve ${a} ${b} ${c}
Should Be Equal ${res} ${expected}
python -m robot .\tests\robot\test_quadratic_lists.robot
========================================================================
====== Test Quadratic Lists
========================================================================
====== Two Roots | PASS |
------------------------------------------------------------------------
------ Single Root | PASS |
------------------------------------------------------------------------
------ No Roots | PASS |
------------------------------------------------------------------------
------ Test Quadratic Lists | PASS | 3 tests, 3 passed, 0 failed
========================================================================
Поменяем тип возвращаемого значения на кортеж
Пример 3.1 PyTest
Всё, что нужно изменить в PyTest коде — квадратные скобки на круглые. [4, -1]
на (4, -1)
и так далее. В нашем примере это замена шести символов.
# test_quadratic.py
import pytest
from quadratic import quadratic_solve
@pytest.mark.parametrize("args, expected_result", [
pytest.param((1, -3, -4), (4, -1), id="two roots",),
pytest.param((1, -2, 1), (1.0, None), id="single root",),
pytest.param((0, 0, 0), (None, None), id="no roots",)
])
def test_solution(args, expected_result):
res = quadratic_solve(*args)
assert res == expected_result
Пример 3.1 RobotFramework
Роботу сложнее приспособиться к кортежам.
Пришлось написать два новых ключевых слова. В сумме прибавилось более десяти строк кода.
*** Settings ***
Library ../../quadratic.py
Test Template Verify Solution
Suite Setup Prepare Variables
*** Variables ***
@{roots1}= ${4.0} ${-1.0}
@{roots2}= ${1.0} ${None}
@{roots3}= ${None} ${None}
*** Test Cases *** a b c expected_result
Two Roots ${1} ${-3} ${-4} ${ex_res1}
Single Root ${1} ${-2} ${1} ${ex_res2}
No Roots ${0} ${0} ${0} ${ex_res3}
*** Keywords ***
Verify Solution
[Arguments] ${a} ${b} ${c} ${expected}
${res}= Quadratic Solve ${a} ${b} ${c}
Should Be Equal ${res} ${expected}
Convert List To Tuple
[Arguments] ${list}
${tuple}= Evaluate tuple(${list})
RETURN ${tuple}
Prepare Variables
${tup1}= Convert List To Tuple ${roots1}
${tup2}= Convert List To Tuple ${roots2}
${tup3}= Convert List To Tuple ${roots3}
Set Suite Variable ${ex_res1} ${tup1}
Set Suite Variable ${ex_res2} ${tup2}
Set Suite Variable ${ex_res3} ${tup3}
Этот пример не показал разницы именно в параметризации, но уже продемонстрировал большую гибкость PyTest при смене типа данных.
Следующий пример является ключевым — в нём мы будем тестировать набор данных заранее неизвестной длины. PyTest справляется с этим легко. А вот роботу придется приделывать костыль, так как здесь нельзя написать конечный набор тестов и расслабиться.

Пример 4. Проверка списка заранее неизвестной длины
Функция get_digits() из модуля list_of_digits.py должна возвращать произвольный набор цифр.
Повторы разрешены.
Цифры это 0, 1, 2… 9
.
Функция написана с ошибкой, но её нелегко поймать одиночным тестом.
Особое внимание хочу обратить на то, что мы параметризуем проверку списка заранее неизвестной длины.
С помощью PyTest можно легко получить заранее неопределённое количество тестов. А если нужно и случайное. Как в нашем примере.
Структура проекта:
list_of_digits/
├── list_of_digits.py
├── tests
│ ├── pytest
│ │ └── test_list_of_digits.py
│ └── robot
│ └── test_list_of_digits.robot
└── venv
# list_of_digits.py
import random
def get_digits() -> list:
n = random.randint(1, 10)
result = []
for i in range(0, n):
result.append(random.randint(4, 10))
return result
Чтобы тест был веселее, функция возвращает списки псевдослучайной длины. Чем список длиннее, тем больше шансов поймать баг.
Пример 4. PyTest
Рассмотрим полный код теста:
# test_list_of_digits.py
import pytest
import random
from list_of_digits import get_digits
digits = get_digits()
@pytest.mark.parametrize("digit", digits)
def test_if_digit(digit):
assert 0 <= digit < 10
Запустим тесты из директории list_of_digits несколько раз пока не поймаем ошибку.
python -m pytest -v --no-header .\tests\pytest\test_list_of_digits.py
=============== test session starts ===============
collected 2 items
tests/pytest/test_list_of_digits.py::test_if_digit[8] PASSED [ 50%]
tests/pytest/test_list_of_digits.py::test_if_digit[7] PASSED [100%]
================ 2 passed in 0.01s ================
=============== test session starts ===============
collected 10 items
tests/pytest/test_list_of_digits.py::test_if_digit[9_0] PASSED [ 10%]
tests/pytest/test_list_of_digits.py::test_if_digit[4] PASSED [ 20%]
tests/pytest/test_list_of_digits.py::test_if_digit[7] PASSED [ 30%]
tests/pytest/test_list_of_digits.py::test_if_digit[8_0] PASSED [ 40%]
tests/pytest/test_list_of_digits.py::test_if_digit[9_1] PASSED [ 50%]
tests/pytest/test_list_of_digits.py::test_if_digit[8_1] PASSED [ 60%]
tests/pytest/test_list_of_digits.py::test_if_digit[6_0] PASSED [ 70%]
tests/pytest/test_list_of_digits.py::test_if_digit[8_2] PASSED [ 80%]
tests/pytest/test_list_of_digits.py::test_if_digit[10] FAILED [ 90%]
tests/pytest/test_list_of_digits.py::test_if_digit[6_1] PASSED [100%]
===================== FAILURES =====================
В первом тест‑ране был создан список из двух элементов. Его проверка не выявила ошибок.
Благодаря @pytest.mark.parametrize
каждая проверка была выполнена как отдельный тест. Поэтому мы видим два результата PASSED
.
Во втором тест‑ране список был уже из десяти элементов. Мы проверили каждый элемент из списка.
Девятый элемент оказался равен 10, а это не цифра. Поэтому тест упал.
Из этого теста хорошо видно, что PyTest делает с аргументами, переданными в декоратор.
@pytest.mark.parametrize("digit", digits)
Первый аргумент хотя и передается как строка — становится одноименной переменной, отвечающей за итерацию по второму аргументу.
Я специально сделал пример с простейшей итерацией обычной переменной по списку.
Без PyTest мы могли бы написать следующую проверку:
for digit in digits:
assert 0 <= digit <= 9, f"{digit} is not a digit"
Но она бы падала при первой ошибке а с PyTest мы видим все падения.
Пример 4. RobotFramework
Как лаконично решить эту задачу на роботе пока непонятно. Ниже предлагаю решение с использованием DataDriver.
Суть в том, что полученный список сохраняется как .csv файл и затем с помощью DataDriver прогоняются отдельные тесты на каждую строку.
Установить его нужно отдельно, так как в стандартную библиотеку робота он не входит.
python -m pip install robotframework-datadriver
Для сохранения в .csv я сделал кастомную .py библиотеку. Скорее всего это можно сделать и на чистом роботе. Но суть не меняется — мы вынуждены добавить дополнительное действие в наш тест.
list_of_digits/
├── list_of_digits.py
├── tests
│ ├── pytest
│ │ └── test_list_of_digits.py
│ └── robot
│ ├── libraries
│ │ └── save_to_csv.py
│ └── test_list_of_digits.robot
└── venv
После запуска теста рядом с list_of_digits.py будет создан файл UserData.csv
# save_to_csv.py
def save_list_to_csv(list_of_digits):
with open("UserData.csv", "w") as f:
f.write("*** Test Cases ***;${var}")
i = 0
for digit in list_of_digits:
i += 1
with open("UserData.csv", "a") as f:
f.writelines("\n")
f.writelines("Test-" + str(i) + ". Digit: " + str(digit))
f.writelines(";")
f.writelines(str(digit))
# test_list_of_digits.py
*** Settings ***
Library DataDriver ../../UserData.csv
Library ../../list_of_digits.py
Library libraries/save_to_csv.py
Suite Setup Prepare Variables
Test Template Variable Is Digit
*** Test Cases ***
Test ${var}
*** Keywords ***
Prepare Variables
${list_of_digits}= Get Digits
Save List To Csv ${list_of_digits}
Variable Is Digit
[Arguments] ${var}
${num}= Convert To Integer ${var}
Should Be True ${num} >= 0
Should Be True ${num} < 10
python -m robot .\tests\robot\test_list_of_digits.robot
========================================================================
====== Test List Of Digits
========================================================================
====== Test-1. Digit: 9 | PASS |
------------------------------------------------------------------------
------ Test-2. Digit: 4 | PASS |
------------------------------------------------------------------------
------ Test-3. Digit: 7 | PASS |
------------------------------------------------------------------------
------ Test-4. Digit: 7 | PASS |
------------------------------------------------------------------------
------ Test List Of Digits | PASS | 4 tests, 4 passed, 0 failed
========================================================================
========================================================================
====== Test List Of Digits
========================================================================
====== Test-1. Digit: 8 | PASS |
------------------------------------------------------------------------
------ Test-2. Digit: 10 | FAIL | '10 < 10' should be true.
------------------------------------------------------------------------
------ Test-3. Digit: 1 | PASS |
------------------------------------------------------------------------
------ Test List Of Digits | FAIL | 3 tests, 2 passed, 1 failed
========================================================================
Первый Тест-ран состоял из четырёх тест‑кейсов и не смог поймать ошибку. Второй тест‑ран хотя и был короче, но оказался удачливее и уже второе число было не цифрой.
Вывод о простоте параметризации следующий: нам удалось добиться от робота выполнения переменного числа тестов, но это далось довольно дорогой ценой.
Если вы знаете более элегантный способ — пишите в комментариях.
Пример 5. Проверка сайта
Практический пример применения параметризации для проверки ответов страниц сайта. Именно при решении этой задачи я столкнулся с необходимостью создавать заранее неизвестное число тестов.
Задача проверить все ли страницы сайта testsetup.ru отвечают HTTP кодом 200.
Структура проекта:
from_site_map/
├── app
│ ├── site_map.py
│ └── tests
│ ├── pytest
│ │ └── test_by_site_map.py
│ └── robot
│ ├── libraries
│ │ └── save_to_csv.py
│ └── test_by_site_map.robot
└── venv
На некоторых хостингах можно бесплатно создать карту своего сайта. Сделать её можно и самостоятельно: скриптом или на стороннем сайте.
Cкачиваем её в ту же директорию где лежит site_map.py:
from_site_map/
├── app
│ ├── site_map.py
│ ├── sitemap.xml
│ └── tests
│ ├── pytest
│ │ └── test_by_site_map.py
│ └── robot
│ ├── libraries
│ │ └── save_to_csv.py
│ └── test_by_site_map.robot
└── venv
Выглядит карта сайта следующим оригинальным образом:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://testsetup.ru/pytest/</loc>
<priority>0.8</priority>
<changefreq>daily</changefreq>
</url>
<url>
<loc>https://testsetup.ru/robot/</loc>
<priority>0.6</priority>
<changefreq>daily</changefreq>
</url>
</urlset>
Только страниц там гораздо больше. Теперь можно переходить к тестам.
Пример 5. PyTest
Для парсинга карты сайта будем использовать библиотеку xmlschema а для обращения к сайту — библиотеку requests.
# site_map.py
import xml.etree.ElementTree as ET
def get_urls() -> list:
tree = ET.parse("sitemap.xml")
root = tree.getroot()
urls = []
for child in root:
for gc in child:
tag = gc.tag
if "loc" in tag:
urls.append(gc.text)
return urls
Тест будет очень лаконичным. За это мы и любим PyTest.
# test_by_site_map.py
import pytest
import requests
from site_map import get_urls
endpoints = get_urls()
@pytest.mark.parametrize('endpoint', endpoints)
def test_status(endpoint):
r = requests.get(endpoint, allow_redirects=False)
assert r.status_code == 200
Запуск теста выполним следующей командой:
python -m pytest -v --no-header tests\pytest\test_by_site_map.py
tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/auto/] PASSED [ 0%]
tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/theory/] PASSED [ 0%]
tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/robot/] PASSED [ 0%]
tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/pytest/] PASSED [ 1%]
tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/testcomplete/] PASSED [ 1%]
...
tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/edu/ PASSED [ 99%]
tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/selenium/] PASSED [100%]
=================================== 553 passed in 121.11s (0:02:01) ====================================
Пример 5. RobotFramework
Для парсинга карты сайта будем использовать библиотеку XML а для обращения к сайту — библиотеку RequestsLibrary.
*** Settings ***
Library XML
Library RequestsLibrary
Library Collections
Library DataDriver ../../Urls.csv
Library libraries/save_to_csv.py
Suite Setup Prepare Variables
Test Template Status Is OK
*** Variables ***
${XML}= sitemap.xml
@{urls}
*** Test Cases ***
Test ${url}
*** Keywords ***
Prepare Variables
${urls}= Get Urls
Save List To Csv ${urls}
Status Is OK
[Arguments] ${url}
${response}= GET ${url} expected_status=200 allow_redirects=${False}
Get Urls
@{children} = Get Elements ${XML} */loc
FOR ${child} IN @{children}
Append To List ${urls} ${child.text}
END
RETURN ${urls}
# save_to_csv.py
def save_list_to_csv(list_of_urls, file_name="Urls.csv"):
with open(file_name, "w") as f:
f.write("*** Test Cases ***;${url}")
i = 0
for url in list_of_urls:
i += 1
with open(file_name, "a") as f:
f.writelines("\n")
f.writelines("Test-" + str(i) + ". URL: " + str(url))
f.writelines(";")
f.writelines(str(url))
python -m robot .\tests\robot\test_site_map.robot
========================================================================
====== Test Site Map
========================================================================
======
------------------------------------------------------------------------
------ Test-1. URL: https://testsetup.ru/testcomplete/ | PASS |
------------------------------------------------------------------------
------ Test-2. URL: https://testsetup.ru/edu/ | PASS |
...
------------------------------------------------------------------------
------ Test Site Map | PASS | 553 tests, 553 passed, 0 failed
========================================================================
Если показать тесты менеджеру, он увидит, что на входе у нас sitemap.xml.
А на выходе:
tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/robot/tags/] PASSED [ 0%]
...
tests/pytest/test_by_site_map.py::test_status[https://testsetup.ru/selenium/] PASSED [100%]
============================== 553 passed in 121.11s (0:02:01) ===============================
Против:
====== Test Site Map
========================================================================
======
------------------------------------------------------------------------
------ Test-1. URL: https://testsetup.ru/testcomplete/ | PASS |
------------------------------------------------------------------------
------ Test-2. URL: https://testsetup.ru/edu/ | PASS |
...
------------------------------------------------------------------------
------ Test Site Map | PASS | 553 tests, 553 passed, 0 failed
========================================================================
Возможные проблемы
Если Python не находит модуль с кодом, можно попробовать перейти в директорию, родительскую по отношению к tests — в нашем последнем примере это from_site_map и добавить ее в системный путь.
В PowerShell команда будет выглядеть так:
$Env:Path += ";$pwd"
В Bash немного по‑другому.
export PATH=$PATH:$(pwd)
Заключение
Robot Framework хорошо справляется с параметризацией тестов, если их число заранее известно. У меня нет однозначного ответа какой инструмент лучше для таких задач.
В более сложных ситуациях Robot Framework проигрывает PyTest в простоте и изящности параметризации. Но повторить суть, по крайней мере в нашем примере, он сумел. Если вы знаете более эффективный способ применения Robot Framework к таким задачам или сталкивались с более показательными примерами различий этих инструментов, будет очень интересно про это узнать в комментариях.